Select
Picks one value from a list. The trigger looks like Input, in the same four sizes; the list opens over it on the raised surface, with the current value aligned to the trigger and marked with a check.
Preview
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
<div className="flex w-full max-w-sm flex-col gap-xs">
<label htmlFor="preview-select" className="text-sm text-text-primary">Role</label>
<Select>
<SelectTrigger id="preview-select">
<SelectValue placeholder="Choose a role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="viewer">Viewer</SelectItem>
<SelectItem value="editor">Editor</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="owner">Owner</SelectItem>
</SelectContent>
</Select>
</div>Usage
When to use
- Pick one value from a known list of options, such as the role of a team member.
- Fit a choice into the space of one field, next to an Input or Button of the same size.
- A longer list split into labelled groups, with a line between them.
When not to use
- A yes or no choice. Use a native checkbox.
- Going to another page or running an action. Use a link or Button.
- Text the user types freely. Use Input.
Variants
size
Trigger height from size-element-*, set on SelectTrigger: sm 28px, md 32px (the default), lg 36px, xl 44px with a mouse; on touch md is 36px, lg 44px and xl 48px. Match the Input or Button next to it: md in forms, sm only in dense mouse-first UI, lg or xl for the main field of a screen.
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
<div className="flex w-full max-w-sm flex-col gap-xs">
<label htmlFor="preview-select-size-sm" className="text-sm text-text-primary">Role</label>
<Select>
<SelectTrigger id="preview-select-size-sm" size="sm">
<SelectValue placeholder="Choose a role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="viewer">Viewer</SelectItem>
<SelectItem value="editor">Editor</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="owner">Owner</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex w-full max-w-sm flex-col gap-xs">
<label htmlFor="preview-select-size-md" className="text-sm text-text-primary">Role</label>
<Select>
<SelectTrigger id="preview-select-size-md">
<SelectValue placeholder="Choose a role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="viewer">Viewer</SelectItem>
<SelectItem value="editor">Editor</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="owner">Owner</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex w-full max-w-sm flex-col gap-xs">
<label htmlFor="preview-select-size-lg" className="text-sm text-text-primary">Role</label>
<Select>
<SelectTrigger id="preview-select-size-lg" size="lg">
<SelectValue placeholder="Choose a role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="viewer">Viewer</SelectItem>
<SelectItem value="editor">Editor</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="owner">Owner</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex w-full max-w-sm flex-col gap-xs">
<label htmlFor="preview-select-size-xl" className="text-sm text-text-primary">Role</label>
<Select>
<SelectTrigger id="preview-select-size-xl" size="xl">
<SelectValue placeholder="Choose a role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="viewer">Viewer</SelectItem>
<SelectItem value="editor">Editor</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="owner">Owner</SelectItem>
</SelectContent>
</Select>
</div>Examples
Default
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
<div className="flex w-full max-w-sm flex-col gap-xs">
<label htmlFor="member-role" className="text-sm text-text-primary">Role</label>
<Select>
<SelectTrigger id="member-role">
<SelectValue placeholder="Choose a role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="viewer">Viewer</SelectItem>
<SelectItem value="editor">Editor</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
</SelectContent>
</Select>
</div>Grouped options
Each group is labelled by its heading; the line between groups is decorative.
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue } from "@/components/ui/select"
<div className="flex w-full max-w-sm flex-col gap-xs">
<label htmlFor="member-access" className="text-sm text-text-primary">Access</label>
<Select>
<SelectTrigger id="member-access">
<SelectValue placeholder="Choose access" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Workspace</SelectLabel>
<SelectItem value="viewer">Viewer</SelectItem>
<SelectItem value="editor">Editor</SelectItem>
</SelectGroup>
<SelectSeparator />
<SelectGroup>
<SelectLabel>Billing</SelectLabel>
<SelectItem value="billing-viewer">Billing viewer</SelectItem>
<SelectItem value="billing-admin">Billing admin</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>Disabled
Keeps its value visible but can't be opened.
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
<div className="flex w-full max-w-sm flex-col gap-xs">
<label htmlFor="locked-role" className="text-sm text-text-primary">Role</label>
<Select defaultValue="owner" disabled>
<SelectTrigger id="locked-role">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="owner">Owner</SelectItem>
</SelectContent>
</Select>
</div>Invalid
The danger border plus error text linked with aria-describedby.
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
<div className="flex w-full max-w-sm flex-col gap-xs">
<label htmlFor="invite-role" className="text-sm text-text-primary">Role</label>
<Select>
<SelectTrigger id="invite-role" aria-invalid aria-describedby="invite-role-error">
<SelectValue placeholder="Choose a role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="viewer">Viewer</SelectItem>
<SelectItem value="editor">Editor</SelectItem>
</SelectContent>
</Select>
<p id="invite-role-error" className="text-sm text-text-danger">Choose a role for the new member.</p>
</div>Do and don't
Do
- Use Select to pick one value from a known list of options.
- Always pair the trigger with a visible label (htmlFor / id).
- Keep option labels short and in sentence case, and sort them in a way the user expects (by order of use, or alphabetically).
- Use a placeholder only to hint at the action (Choose a role), never instead of the label.
- Match the trigger size to the Input or Button next to it.
- Split a long list into groups with a SelectLabel heading each (Workspace, Billing), and a SelectSeparator between them.
Don't
- Don't use Select for a yes or no choice. Use a checkbox.
- Don't use Select to navigate or to trigger actions. Use links or Button.
- Don't put icons, descriptions or other controls inside options; an option is one line of text.
- Don't use size sm on touch-first screens.
- Don't restyle colors through className. If a state looks wrong, open an issue instead of overriding it.
Props
| Name | Type | Values | Default | Description |
|---|---|---|---|---|
| size | enum | sm | md | lg | xl | md | Trigger height from size-element-*, set on SelectTrigger: sm 28px, md 32px (the default), lg 36px, xl 44px with a mouse; on touch md is 36px, lg 44px and xl 48px. Match the Input or Button next to it: md in forms, sm only in dense mouse-first UI, lg or xl for the main field of a screen. |
| disabled | boolean | - | false | Set on Select. The trigger can't be focused or opened and uses the neutral disabled look. |
| placeholder | string | - | - | Set on SelectValue. Shown in the trigger until a value is picked. Not a label. |
| defaultValue | string | - | - | Set on Select. The value picked on first render, when you don't control it. |
| value | string | - | - | Set on Select. The picked value, when you control it. Pair it with onValueChange. |
| onValueChange | function | - | - | Set on Select. Called with the new value when the user picks an option. |
| name | string | - | - | Set on Select. Submits the value with a form under this name. Only on web. |
| required | boolean | - | false | Set on Select. A form can't be submitted until a value is picked. Say it in the label too, not only with this prop. Only on web. |
| open | boolean | - | - | Set on Select. Whether the list is open, when you control it. Pair it with onOpenChange. |
| defaultOpen | boolean | - | false | Set on Select. Opens the list on first render, when you don't control it. |
| onOpenChange | function | - | - | Set on Select. Called with true or false when the list opens or closes. |
| dir | enum | ltr | rtl | - | Set on Select. Reading direction of the list and its keyboard. Leave it out to follow the page. Only on web. |
Slots
| Name | Required | Description |
|---|---|---|
| trigger | yes | SelectTrigger with a SelectValue inside. |
| content | yes | SelectContent with one SelectItem per option. Item labels are short nouns in sentence case. Long lists: SelectGroup with a SelectLabel per group, SelectSeparator between groups. |
Anatomy
| Part | Description |
|---|---|
| root | Select. Holds the value and the open state; renders nothing itself. |
| trigger | SelectTrigger: the button that shows the value and opens the list. Same fill, border and sizes as Input. |
| value | SelectValue: the current option's text, or the placeholder when nothing is picked. |
| icon | Tabler ChevronDownIcon at the end of the trigger. 16px in sm, md and lg, 20px in xl (DESIGN.md 9.1). |
| content | SelectContent: the floating list on the raised surface, at least as wide as the trigger. Long lists scroll inside it. |
| item | SelectItem: one option. Highlighted on hover and with the arrow keys. |
| indicator | Tabler CheckIcon (16px) at the end of the selected option. |
| optionGroup | SelectGroup: options under one heading. Renders no box of its own. |
| groupLabel | SelectLabel: the heading of a group, smaller than an option, in secondary text. Not selectable. |
| separator | SelectSeparator: a line across the full list between two groups. |
| scrollButton | SelectContent shows a caret at the top or bottom while more options are out of view; pointing at it scrolls the list. Tabler ChevronUpIcon or CaretDownIcon, 16px. |
| hitArea | Transparent ::before centered on the trigger, never smaller than size-target-min (24px on mouse, 44px on touch), so sm stays a 44px touch target. |
Accessibility
Role: combobox
| Keys | Action |
|---|---|
| Tab | Moves focus to the trigger and shows the focus ring. |
| Enter, Space, ArrowDown, ArrowUp | Opens the list with the current value highlighted. |
| ArrowDown, ArrowUp | Moves the highlight while the list is open. |
| Home, End | Highlights the first or last option. |
| Enter, Space | Picks the highlighted option, closes the list and returns focus to the trigger. |
| Escape | Closes the list without changing the value and returns focus to the trigger. |
| A to Z | Highlights the next option that starts with the typed letters. |
- Give the trigger a visible label: an id on SelectTrigger and a label with htmlFor. aria-label only where the label is shown another way, such as a table column header.
- Invalid: aria-invalid on SelectTrigger plus error text linked with aria-describedby. Never color alone.
- Disabled uses the native disabled attribute on the trigger, so it leaves the tab order.
- The trigger is a button with role combobox and the list a listbox; options are announced with their selected state.
- Single value only. For several values use a group of checkboxes.
- SelectGroup has role group and is labelled by its SelectLabel, so a screen reader announces the heading when the highlight enters the group. SelectLabel is not focusable and the arrow keys skip it.
- SelectSeparator is decorative (aria-hidden).
Install
npx shadcn add https://werewolf-ui.vercel.app/r/select.jsonThis also adds styles/werewolf.css, the tokens every component uses, and any other component it needs. Import that file once in your root CSS, after @import "tailwindcss", with a path relative to that CSS file (from app/globals.css it is @import "../styles/werewolf.css").