Select
A trigger that shows the chosen value and opens a list to pick another, with an optional footer for custom values.
import { Select } from "@butteryui/react";Demo
When to use it
Use it for
- A value a control shows and keeps, picked from a list: a canvas shape, an export format
- A list of presets with a custom value beside them, through footer
- A form field: inside a Field, which shows the label (label alone names it for screen readers), with variant="outline", or variant="soft" beside soft text fields
Not for
- Two to four options that fit side by side: use SegmentedControl
- Actions rather than a value: use Menu
- A choice in a form that waits for submit with many options: a native select styled with bt-control
Examples
Canvas size
Preset shapes with width and height fields underneath; typing a size makes it custom.
Deck font
A select that keeps its own value, starting on the deck's sans font; the trigger shows each font picked.
Export format
A small select for an export format, typed to its values, with the file name it leads to.
Saves as title-slide.png
Variants and sizes
The ghost pill for toolbars, and the outline and soft boxes for forms, at their medium and small sizes.
Variants and states
Canvas shape
A ghost pill showing the value; the list lines up with its end and checks the chosen option.
With a footer
Width and height fields under the list for a custom size; Tab moves between the list and the fields.
Small, placeholder and disabled
Outline and soft boxes
For forms, a box like a text field's that fills its column, at 34px or 28px: outline with a 1px edge, or soft on bg-control, its edge showing on hover, while it has focus and while its list is open.
Props
Select
| Prop | Type | Default | Description |
|---|---|---|---|
labelrequired | string | What the value is, such as "Canvas shape". It names the trigger, the list and the footer for screen
readers and isn't shown; a Field around the select shows its label above the box. | |
optionsrequired | readonly SelectOption<Value>[] | The choices, in order. | |
value | Value | The chosen value. Pair with onValueChange to control it. | |
defaultValue | Value | The value chosen at first when uncontrolled. | |
onValueChange | ((value: Value) => void) | Called with the value of the option chosen. | |
open | boolean | Whether the list is open. Pair with onOpenChange to control it. | |
defaultOpen | boolean | false | Whether the list starts open when uncontrolled. Defaults to false. |
onOpenChange | ((open: boolean) => void) | Called when the list opens or closes. | |
valueLabel | string | What the trigger shows instead of the chosen option's label, such as a custom size from the footer. | |
placeholder | string | "Choose" | What the trigger shows while nothing is chosen. Defaults to "Choose". |
footer | ReactNode | Controls under the list, such as width and height fields for a custom value. | |
footerLabel | string | label | Names the footer for screen readers. Defaults to label. |
align | "start" | "center" | "end" | "end" | Which edge of the trigger the list lines up with. Defaults to "end", or "start" for a box. |
variant | "ghost" | "outline" | "soft" | "ghost" | How the trigger looks: "ghost" (the default), a pill for header bars and toolbars; "outline", a box like
a text field's that fills its column, for forms; or "soft", the same box on bg-control, its edge showing
only on hover, while it has focus and while the list is open. A box's list is at least as wide as the box. |
size | "sm" | "md" | "md" | The trigger's height: "sm" (28px) or "md" (36px as a pill, 34px as a box). Defaults to "md". |
It also takes Omit<ComponentPropsWithRef<"button">, "value" | "defaultValue" | "onChange" | "children">.
SelectOption
One choice in a `Select`.
| Field | Type | Default | Description |
|---|---|---|---|
valuerequired | Value | What choosing it sets the select's value to. | |
labelrequired | string | What the list and, once chosen, the trigger show. | |
disabled | boolean | Keeps it from being chosen; arrow keys skip it. |
Accessibility
The trigger is a select-only combobox, a button with role="combobox", named by the label and the value ("Canvas shape, 16:9"), with aria-haspopup="listbox", aria-expanded and aria-controls; in a required Field it has aria-required too. The list is a listbox of options with aria-selected; it opens with focus on the chosen option. Arrow keys, Home and End move, typing jumps, Enter or Space chooses, Escape closes, and focus goes back to the trigger as it closes. A footer is a group named by footerLabel, and Tab loops between the list and its controls.
| Keys | What they do |
|---|---|
| Enter, Space, Arrow Down or Arrow Up | Opens the list from the trigger, with focus on the chosen option, or the first when none is chosen |
| Arrow Down or Arrow Up | Moves to the next or previous option, skipping disabled ones |
| Home or End | Moves to the first or last option |
| Letters | Jumps to the next option that starts with them; letters typed within half a second search as one |
| Enter or Space | Chooses the focused option, closes the list and puts focus back on the trigger |
| Escape | Closes the list without choosing and puts focus back on the trigger |
| Tab | Moves between the list and the footer's controls when there's a footer, and stays on the list when there isn't |
Motion
It appears with a `Presence` preset and leaves faster than it came, in about 60% of the time.
With reduced motion: The list fades in and out over 100ms, without growing from the trigger.
Layout
The ghost trigger is a pill --bt-h-button (36px) tall, or --bt-h-sm (28px) with size="sm", as wide as its value and chevron; the outline and soft boxes are --bt-h-control (34px) or --bt-h-sm tall and fill their column, a soft box keeping its edge transparent until hover, focus or its list opens. The value truncates with an ellipsis and shows in full as its title, and the whole trigger is the hit area. The list is at least --bt-w-menu-min (200px) wide, and a box's list at least as wide as the box; its options are --bt-h-button tall with labels that truncate, and it scrolls inside when the window leaves it less room than it needs.
Related
See it in the Halfmoon shop demo. See it in the Deckhand demo.