Select
A select is a field where you choose one value from a list. It opens from a button that looks like an Input, and shows the choice once made.
import { Field, FieldLabel } from "@/components/ui/field";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const fruits = [
{ label: "Select a fruit", value: null },
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
];
const plans = [
{ label: "Free", value: "free" },
{ label: "Pro", value: "pro" },
{ label: "Team", value: "team" },
{ label: "Enterprise", value: "enterprise" },
];
export default function SelectDemo() {
return (
<div className="flex w-full max-w-xs flex-col gap-6">
<Field name="fruit">
<FieldLabel>Fruit</FieldLabel>
<Select items={fruits}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Fruits</SelectLabel>
{fruits.slice(1).map((fruit) => (
<SelectItem key={fruit.value} value={fruit.value}>
{fruit.label}
</SelectItem>
))}
</SelectGroup>
<SelectSeparator />
<SelectItem value="grape" disabled>
Grape (out of stock)
</SelectItem>
</SelectContent>
</Select>
</Field>
<Field name="plan">
<FieldLabel>Plan</FieldLabel>
<Select items={plans} defaultValue="pro">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{plans.map((plan) => (
<SelectItem key={plan.value} value={plan.value}>
{plan.label}
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
<Field name="region" disabled>
<FieldLabel>Region</FieldLabel>
<Select items={[{ label: "Europe", value: "eu" }]} defaultValue="eu" disabled>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="eu">Europe</SelectItem>
</SelectContent>
</Select>
</Field>
</div>
);
}The code
The component is a single file, read from the repository when the site is built. Press Show code to see all of it.
import type { ComponentProps } from "react";
import { Check, ChevronsUpDown } from "lucide-react";
import { Select as BaseSelect } from "@base-ui/react/select";
import { cx } from "@/lib/cx";
import { controlStyles, popupMotion, popupSurface } from "@/lib/styles";
import type { WithClass } from "@/lib/types";
// Pass `items` (a list of { label, value }) to Select so the trigger can show the label of the
// chosen value, then render one SelectItem for each.
export const Select = BaseSelect.Root;
// The shown choice, in the secondary color while nothing is chosen yet (the placeholder).
export function SelectValue({ className, ...props }: WithClass<ComponentProps<typeof BaseSelect.Value>>) {
return <BaseSelect.Value className={cx("truncate data-[placeholder]:text-text-secondary", className)} {...props} />;
}
// The button that opens the list. It looks like an Input, with a pair of arrows on the right.
export function SelectTrigger({ className, children, ...props }: WithClass<ComponentProps<typeof BaseSelect.Trigger>>) {
return (
<BaseSelect.Trigger
className={cx(
"inline-flex h-9 cursor-pointer items-center justify-between gap-2 text-left",
controlStyles,
"data-[popup-open]:border-text",
className,
)}
{...props}
>
{children}
<BaseSelect.Icon>
<ChevronsUpDown aria-hidden className="size-4 text-text-secondary" />
</BaseSelect.Icon>
</BaseSelect.Trigger>
);
}
// The list opens below the trigger and is at least as wide as it.
export function SelectContent({
sideOffset = 4,
className,
children,
...props
}: WithClass<ComponentProps<typeof BaseSelect.Popup>> & Pick<ComponentProps<typeof BaseSelect.Positioner>, "sideOffset">) {
return (
<BaseSelect.Portal>
<BaseSelect.Positioner sideOffset={sideOffset} alignItemWithTrigger={false} className="z-50">
<BaseSelect.Popup
className={cx(popupSurface, popupMotion, "min-w-[var(--anchor-width)]", className)}
{...props}
>
<BaseSelect.List className="max-h-[var(--available-height)] overflow-y-auto p-1">{children}</BaseSelect.List>
</BaseSelect.Popup>
</BaseSelect.Positioner>
</BaseSelect.Portal>
);
}
export function SelectItem({ className, children, ...props }: WithClass<ComponentProps<typeof BaseSelect.Item>>) {
return (
<BaseSelect.Item
className={cx(
"grid cursor-default grid-cols-[1rem_1fr] items-center gap-2 rounded-sm py-1.5 pr-3 pl-2 text-base text-text outline-hidden select-none data-[highlighted]:bg-text/10 data-[disabled]:cursor-not-allowed data-[disabled]:text-text-secondary",
className,
)}
{...props}
>
<BaseSelect.ItemIndicator className="col-start-1">
<Check aria-hidden className="size-4" />
</BaseSelect.ItemIndicator>
<BaseSelect.ItemText className="col-start-2">{children}</BaseSelect.ItemText>
</BaseSelect.Item>
);
}
export const SelectGroup = BaseSelect.Group;
export function SelectLabel({ className, ...props }: WithClass<ComponentProps<typeof BaseSelect.GroupLabel>>) {
return <BaseSelect.GroupLabel className={cx("px-2 py-1.5 text-sm font-medium text-text-secondary", className)} {...props} />;
}
export function SelectSeparator({ className, ...props }: WithClass<ComponentProps<typeof BaseSelect.Separator>>) {
return <BaseSelect.Separator className={cx("-mx-1 my-1 h-px bg-border", className)} {...props} />;
}Props
| Prop | Type | Default | What it does |
|---|---|---|---|
items | { label, value }[] | — | On Select. The choices, so the button can show the label of the chosen value. Pass an item with a null value for a placeholder. |
value | string | — | On each SelectItem. What the item stands for. Required. |
defaultValue | string | — | On Select. The value that starts chosen, when it manages its own state. |
onValueChange | function | — | On Select. Called with the newly chosen value. |
disabled | boolean | false | On Select or on one SelectItem. Stops it from being opened or chosen. |
name | string | — | On Select, or on the Field around it. The name the value is submitted under in a form. |
Every other prop goes to the Base UI Select underneath. Wrap it in a Field to give it a label.
The thinking
A select is for choosing one value out of too many to show as a row of radios. It saves space, at the cost of one extra click.
- Built on Base UI: Base UI handles the open state, the highlight, type-ahead, the placement and the hidden form input.
- Not a native select: A native select can't be styled, so the open list would look different on every system. This one looks like the rest of the library.
- Use items: Give Select an items list so the button can show the label of the chosen value, such as Pro, rather than its value.
- Select or Dropdown Menu: A select picks a value for a field. A Dropdown Menu runs commands.
Interface design
- Looks like an Input: The button shares the Input's height, border and focus ring, with a pair of up and down arrows after the text.
- Placeholder: While nothing is chosen, the text is the quiet secondary color, like an Input's placeholder.
- List: The shared overlay surface, at least as wide as the button, opening below it. The chosen row has a tick on the left.
- Disabled: A flat surface with a muted border, as for a disabled Input.
Interaction
- Open: Click the button, or press Enter, Space or the Down arrow when it has focus.
- Keyboard: The arrow keys move the highlight, and typing a letter jumps to a row that starts with it. Enter or Space chooses. A disabled row can be highlighted but not chosen.
- Close: Choosing a row, Escape, or a click outside. Focus goes back to the button.
- Open state: While the list is open, the border of the button brightens.
Empathy
- Labeled: Wrap it in a Field with a FieldLabel so the button is announced with a name.
- Announced as a listbox: The list reports its options, and which one is selected.
- Few choices?: With five or fewer options, a RadioGroup shows them all at once and takes fewer clicks.