Select
freeSelect lets users choose one option or several from a searchable list.
@floating-ui/react
Preview
Dark
import Select from '@/components/ui/Select';
const frameworks = [
{ value: 'next', label: 'Next.js' },
{ value: 'remix', label: 'Remix' },
{ value: 'astro', label: 'Astro' },
{ value: 'nuxt', label: 'Nuxt' },
{ value: 'sveltekit', label: 'SvelteKit' },
];
export default function UsageDemo() {
return (
<div className="w-full max-w-sm">
<Select placeholder="Select a framework" options={frameworks} />
</div>
);
}Installation
Add this component with the NateUI CLI.
npx nateui@latest add SelectExamples
Basic
Preview
Dark
import Select from '@/components/ui/Select';
const roles = [
{ value: 'owner', label: 'Owner' },
{ value: 'admin', label: 'Admin' },
{ value: 'editor', label: 'Editor' },
{ value: 'viewer', label: 'Viewer' },
];
export default function BasicDemo() {
return (
<div className="w-full max-w-sm">
<Select
placeholder="Assign a role"
defaultValue={roles[2]}
options={roles}
/>
</div>
);
}Multiple
Preview
Dark
TypeScriptReact
import Select from '@/components/ui/Select';
const skills = [
{ value: 'ts', label: 'TypeScript' },
{ value: 'react', label: 'React' },
{ value: 'node', label: 'Node.js' },
{ value: 'go', label: 'Go' },
{ value: 'rust', label: 'Rust' },
{ value: 'python', label: 'Python' },
];
export default function MultipleDemo() {
return (
<div className="w-full max-w-sm">
<Select.Multi
placeholder="Select skills"
defaultValue={[skills[0], skills[1]]}
options={skills}
/>
</div>
);
}Size
Preview
Dark
import Select from '@/components/ui/Select';
const timezones = [
{ value: 'utc', label: 'UTC' },
{ value: 'est', label: 'Eastern (EST)' },
{ value: 'pst', label: 'Pacific (PST)' },
{ value: 'cet', label: 'Central Europe (CET)' },
];
export default function SizeDemo() {
return (
<div className="flex w-full max-w-sm flex-col gap-4">
<Select size="sm" placeholder="Small" options={timezones} />
<Select size="md" placeholder="Medium" options={timezones} />
<Select size="lg" placeholder="Large" options={timezones} />
</div>
);
}Searchable
Preview
Dark
import Select from '@/components/ui/Select';
const countries = [
{ value: 'us', label: 'United States' },
{ value: 'ca', label: 'Canada' },
{ value: 'gb', label: 'United Kingdom' },
{ value: 'de', label: 'Germany' },
{ value: 'fr', label: 'France' },
{ value: 'jp', label: 'Japan' },
{ value: 'sg', label: 'Singapore' },
{ value: 'au', label: 'Australia' },
];
export default function SearchableDemo() {
return (
<div className="w-full max-w-sm">
<Select
isSearchable
placeholder="Select a country"
options={countries}
searchInputProps={{ placeholder: 'Search countries...' }}
/>
</div>
);
}Disabled
Preview
Dark
import Select from '@/components/ui/Select';
const plans = [
{ value: 'free', label: 'Free' },
{ value: 'pro', label: 'Pro' },
{ value: 'team', label: 'Team', disabled: true },
{ value: 'enterprise', label: 'Enterprise', disabled: true },
];
export default function DisabledDemo() {
return (
<div className="flex w-full max-w-sm flex-col gap-4">
<Select placeholder="Some plans unavailable" options={plans} />
<Select isDisabled defaultValue={plans[1]} options={plans} />
</div>
);
}Grouped
Preview
Dark
import Select from '@/components/ui/Select';
const groupedOptions = [
{
label: 'Frontend',
options: [
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue' },
{ value: 'svelte', label: 'Svelte' },
],
},
{
label: 'Backend',
options: [
{ value: 'node', label: 'Node.js' },
{ value: 'django', label: 'Django' },
{ value: 'rails', label: 'Rails' },
],
},
];
export default function GroupedDemo() {
return (
<div className="w-full max-w-sm">
<Select<object>
placeholder="Select a technology"
options={groupedOptions}
formatGroupLabel={(group) => (
<div className="px-1 py-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
{group.label}
</div>
)}
/>
</div>
);
}Custom Options
Preview
Dark
import Select from '@/components/ui/Select';
import Avatar from '@/components/ui/Avatar';
const members = [
{ value: 'aria', label: 'Aria Patel', avatar: '/img/avatars/thumb-5.jpg' },
{ value: 'diego', label: 'Diego Ramos', avatar: '/img/avatars/thumb-6.jpg' },
{ value: 'mei', label: 'Mei Lin', avatar: '/img/avatars/thumb-7.jpg' },
{ value: 'sam', label: 'Sam Turner', avatar: '/img/avatars/thumb-8.jpg' },
];
function MemberAvatar({ src, alt }: { src: string; alt: string }) {
return <Avatar size={22} src={src} alt={alt} />;
}
export default function CustomOptionsDemo() {
return (
<div className="w-full max-w-sm">
<Select
placeholder="Assign a member"
defaultValue={members[0]}
options={members}
customInputDisplay={(selectedItem) =>
selectedItem ? (
<Select.ValueWithPrefix
prefix={
<MemberAvatar
src={selectedItem.avatar}
alt={selectedItem.label}
/>
}
label={selectedItem.label}
/>
) : null
}
customOption={({ option, selected, CheckIcon }) => (
<Select.OptionWithPrefix
prefix={<MemberAvatar src={option.avatar} alt={option.label} />}
label={option.label}
selected={selected}
checkIcon={CheckIcon}
/>
)}
/>
</div>
);
}Creatable
Preview
Dark
import { useState } from 'react';
import Select from '@/components/ui/Select';
type Tag = { value: string; label: string };
const initialTags: Tag[] = [
{ value: 'bug', label: 'bug' },
{ value: 'feature', label: 'feature' },
{ value: 'docs', label: 'docs' },
];
export default function CreatableDemo() {
const [tags, setTags] = useState<Tag[]>(initialTags);
const [selected, setSelected] = useState<Tag[]>([]);
const handleChange = (next: Tag[]) => {
const known = new Set(tags.map((tag) => tag.value));
const created = next.filter((tag) => !known.has(tag.value));
if (created.length) {
setTags((prev) => [...prev, ...created]);
}
setSelected(next);
};
return (
<div className="w-full max-w-sm">
<Select.Multi
isCreatable
isSearchable
placeholder="Select or create tags"
options={tags}
value={selected}
onChange={handleChange}
searchInputProps={{ placeholder: 'Type to search or create...' }}
/>
</div>
);
}Async
Preview
Dark
import { useState } from 'react';
import Select from '@/components/ui/Select';
type Repo = { value: string; label: string };
const allRepos: Repo[] = [
{ value: 'core', label: 'platform-core' },
{ value: 'ui', label: 'design-system' },
{ value: 'docs', label: 'docs-site' },
{ value: 'infra', label: 'infra-tooling' },
];
export default function AsyncDemo() {
const [repos, setRepos] = useState<Repo[]>([]);
const [loading, setLoading] = useState(false);
const loadOnOpen = () => {
if (repos.length > 0) {
return;
}
setLoading(true);
// Mock a request; replace with a real fetch in your app.
setTimeout(() => {
setRepos(allRepos);
setLoading(false);
}, 1200);
};
return (
<div className="w-full max-w-sm">
<Select
placeholder="Select a repository"
options={repos}
isLoading={loading}
onMenuOpen={loadOnOpen}
noOptionsMessage={loading ? 'Loading...' : 'No options'}
/>
</div>
);
}Controlled
Preview
Dark
Selected: In progress
import { useState } from 'react';
import Select from '@/components/ui/Select';
type Option = { value: string; label: string };
const statuses: Option[] = [
{ value: 'todo', label: 'To do' },
{ value: 'in-progress', label: 'In progress' },
{ value: 'review', label: 'In review' },
{ value: 'done', label: 'Done' },
];
export default function ControlledDemo() {
const [status, setStatus] = useState<Option>(statuses[1]);
return (
<div className="flex w-full max-w-sm flex-col gap-4">
<Select
placeholder="Select a status"
value={status}
onChange={setStatus}
options={statuses}
/>
<div className="text-sm text-muted-foreground">
Selected:{' '}
<span className="font-semibold text-foreground">{status.label}</span>
</div>
</div>
);
}API
Select ships two controls from one import: Select for a single choice and
Select.Multi for several. Both take the same options shape — a flat array of
{ label, value } items, or an array of { label, options } groups — and share
the search, loading, sizing, and validation props. Select.ValueWithPrefix and
Select.OptionWithPrefix are helpers for the custom render props.
Select
| Prop | Description | Type | Default |
|---|---|---|---|
options | Selectable options: a flat list, or groups of options. | Options<T> | [] |
value | Selected option in controlled mode. | SingleOption<T> | null | - |
defaultValue | Initial selected option when uncontrolled. | SingleOption<T> | null | - |
onChange | Fires with the newly selected option. | (value: SingleOption<T>) => void | - |
placeholder | Text shown when nothing is selected. | string | - |
isSearchable | Show a filter input inside the menu. | boolean | false |
isCreatable | Let the filter text become a new option. | boolean | false |
isDisabled | Disable the whole control. | boolean | false |
isLoading | Show a spinner in the control. | boolean | false |
invalid | Apply the invalid style; also inherited from an invalid Form.Field. | boolean | false |
size | Control size; falls back to InputGroup, Form, then ConfigProvider. | ControlSize | 'md' |
placement | Menu placement relative to the control. | Placement | - |
customInputDisplay | Render the selected value in the control yourself. | (selectedItem: SingleOption<T> | null) => ReactNode | - |
customOption | Render each option row yourself. | CustomOption<T> | - |
formatGroupLabel | Render a group's header label. | (group: GroupOption<T>) => ReactNode | - |
filter | Replace the default option filtering. | FilterFunction<T> | - |
noOptionsMessage | Message shown when no options match. | string | ((ListElement) => ReactNode) | 'No options' |
onInputChange | Fires with the filter text as the user types. | (inputValue: string) => void | - |
onMenuOpen | Fires when the menu opens. | () => void | - |
searchInputProps | Extra props for the filter input. | Omit<ComponentProps<'input'>, 'onChange' | 'value' | 'onKeyDown' | 'ref'> | - |
inputId | id for the underlying input, for label association. | string | - |
className | Class names for the control element. | string | - |
...nativeProps | Native div props (except defaultValue and onChange). | Omit<ComponentPropsWithRef<'div'>, 'defaultValue' | 'onChange'> | - |
Select.Multi
Select.Multi shares every Select prop above except customInputDisplay, and
replaces the single-value props with array equivalents.
| Prop | Description | Type | Default |
|---|---|---|---|
value | Selected options in controlled mode. | SelectedOptions<T> | - |
defaultValue | Initial selected options when uncontrolled. | SelectedOptions<T> | - |
onChange | Fires with the full array of selected options. | (value: SelectedOptions<T>) => void | - |
customLabel | Render each selected tag's content yourself. | (selectedItem: SingleOption<T>) => ReactNode | - |
showClearAllButton | Show the per-tag remove buttons and the clear-all control. | boolean | true |
filter | Replace the default option filtering. | FilterFunctionMulti<T> | - |
Select.ValueWithPrefix
| Prop | Description | Type | Default |
|---|---|---|---|
label | The value's main content. | ReactNode | - |
prefix | Node rendered before the label, such as an avatar or icon. | ReactNode | - |
showPrefix | Whether to render the prefix. | boolean | true |
Select.OptionWithPrefix
| Prop | Description | Type | Default |
|---|---|---|---|
label | The option's main content. | ReactNode | - |
prefix | Node rendered before the label, such as an avatar or icon. | ReactNode | - |
selected | Whether this option is currently selected. | boolean | false |
checkIcon | Node shown on the right when selected; pass the CheckIcon from customOption. | ReactNode | - |
Types
type ControlSize = 'sm' | 'md' | 'lg'
// A single option. Extra keys of T (e.g. an avatar or color) are kept
// on the option and passed back to the custom render props.
type SingleOption<T> = {
label: string
value: any
disabled?: boolean
} & T
// A labelled group of single options.
type GroupOption<T> = {
label: string
options: SingleOption<T>[]
}
// options accepts a flat list or a list of groups.
type Options<T> = SingleOption<T>[] | GroupOption<T>[]
// Select.Multi value / defaultValue.
type SelectedOptions<T> = SingleOption<T>[]
type CustomOption<T> = (props: {
option: SingleOption<T>
hovered: boolean
selected: boolean
CheckIcon: ReactNode
}) => ReactNode
type FilterFunction<T> = (props: {
inputValue: string
options: Options<T>
selectedItem?: SingleOption<T> | null
}) => Options<T>
type FilterFunctionMulti<T> = (props: {
inputValue: string
options: Options<T>
selectedItems: SingleOption<T>[]
}) => Options<T>
// Placement is re-exported from @floating-ui/react:
// 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start'
// | 'bottom-end' | 'left' | 'left-start' | 'left-end'
// | 'right' | 'right-start' | 'right-end'