Skip to content

Select

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/select.json
See it liveClosed, open, grouped, and with more than one chosen.→

The component the oldest rule in the line is about. A native <select> cannot be dressed: the browser draws its popup itself, in the operating system’s chrome, with the operating system’s fonts and spacing, and no CSS reaches inside it. On a screen where every other control is the product’s own, the one native dropdown reads as a foreign object - and on Windows it reads as a foreign object from 1998.

So this renders <button role="combobox"> and a portalled list of role="option", with zero native elements. The test asserts exactly that, because it is the whole reason the component exists, and the ESLint rule dowel/no-native-select stops the native one coming back by hand.

What that costs is everything the browser was doing for free: the keyboard, type-ahead, the announcement of the selected value, the scroll into view, and on a phone the whole native picker. Base UI reimplements all of it, which is the only reason the trade is worth making - a hand-rolled dropdown is how a product ships a control a screen reader cannot see.

The trigger wears Input’s field classes, imported rather than copied, because a select and a text field sit next to each other in every form there has ever been.

SelectValue’s children is a function of the value, not a node. Passing a node pins the trigger to that node forever and the selection never appears; the placeholder goes in placeholder.

And what it shows is the raw value - plum, not Plum - unless the root is given an items map to look the label up in. A product that skips items gets its own identifiers on screen, and it is not obvious why.

Select when the options are few enough to read: a status, a currency, a priority. The reader scans a list and picks.

Combobox when they are not. A Select stops being usable somewhere around thirty options, and a country picker, a tag field or a person picker is well past that - the reader knows what they want and needs to type it rather than hunt for it. Combobox is also the one with chips, so it is the answer for multi-select that has to show what is chosen.

Both support multiple, and both refuse to be a native <select>.

import {
Select,
SelectItem,
SelectPopup,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
<Select items={labels} value={value} onValueChange={setValue}>
<SelectTrigger aria-label={t('priority')}>
<SelectValue placeholder={t('choosePriority')} />
</SelectTrigger>
<SelectPopup>
<SelectItem value="low">{t('low')}</SelectItem>
<SelectItem value="high">{t('high')}</SelectItem>
</SelectPopup>
</Select>

The cva variants (selectTriggerVariants, selectPopupVariants, selectItemVariants) are exported for a product that needs the same clothes somewhere else.

Props

Read from the types in ui/select.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.

Select

The root. multiple turns value into an array; otherwise controlled with value and onValueChange, or left to manage itself.

Base UI's Select.Root, unchanged under dowel's name - its props are on its own page.

SelectValue

What the trigger shows: the selected item's label, and the placeholder the product gives it until there is one.

Every prop of Base UI's Select.Value, passed on.

SelectIcon

The chevron, or whatever the product puts there. Marked decorative by Base UI, since the button is already named by its value.

Base UI's Select.Icon, unchanged under dowel's name - its props are on its own page.

SelectGroup

A labelled group of options.

Base UI's Select.Group, unchanged under dowel's name - its props are on its own page.

SelectItemText

The text of an option, which is what the trigger echoes when it is chosen.

Base UI's Select.ItemText, unchanged under dowel's name - its props are on its own page.

SelectItemIndicator

The tick, drawn only on the chosen option.

Base UI's Select.ItemIndicator, unchanged under dowel's name - its props are on its own page.

SelectTrigger

The control. A <button role="combobox"> - never a <select>.

PropTypeDefault
sizesm | md | lg'md'

And every prop of Base UI's Select.Trigger, passed on.

SelectPopup

The list. Portalled and positioned against the trigger.

PropTypeDefault
sizesm | md | lg'md'
sidebottom | top | left | right | inline-end | inline-startPreferred side of the trigger. Base UI flips it when it does not fit.
aligncenter | start | endAlignment along that side.
sideOffsetnumber | OffsetFunction4Distance from the trigger, in pixels.
containerHTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | nullWhere to portal to. Defaults to the raised host of the overlay this is opened inside (layer.tsx), and to the document body when there is none - either way not the element it was opened from, whose overflow would clip it. Pass an element to put it somewhere else, such as a container being screenshotted.

And every prop of Base UI's Select.Popup, passed on.

SelectItem

An option, with the tick that says it is the chosen one.

PropTypeDefault
indicatorbooleantrueDraw the tick on the chosen item. Pass false for a list where the choice is obvious some other way.

And every prop of Base UI's Select.Item, passed on.

SelectGroupLabel

The caption above a group.

Every prop of Base UI's Select.GroupLabel, passed on.

SelectSeparator

A line between groups of options.

Every prop of Base UI's Select.Separator, passed on.

Accessibility

The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 16 rules applied and passed, and none failed. The accessibility report has every item.

History

Each release that changed ui/select.tsx - the file a product copies - newest first.

  1. v0.41.0
    • docs read every props table from the component's types ee3b159
  2. v0.40.0
    • fix hold every surface over the page to its room on both axes d0436a7
  3. v0.33.0
    • feat one caption utility and the compact rail width 88a3fee
  4. v0.32.0breaking
    • feat a layer for popups inside overlays, a body that scrolls alone 5244cfd
    • fix let an icon keep the size it is given, across the set 217b558
  5. v0.28.0breaking
    • feat density as a property of a region 849d783
  6. v0.26.1
    • fix shade a popup instead of staining the page behind it 9c7a96b
  7. v0.18.1
    • fix five defects a live run found, and none of the tests did 863226c
  8. v0.9.0
    • feat Menu, ContextMenu, Select and Combobox 243cc12