Select
A real <select>, opted into the stylable form with appearance: base-select.
The listbox, the keyboard, typeahead, form participation and the mobile picker
are the platform's.
import { Select } from '@apostel/bedrock'
Source
import { useState } from 'react'
import { Select } from '../../src/index'
/**
* A real `<select>`. On a browser with `appearance: base-select` the trigger
* and the list are styleable in place; everywhere else it falls back to the
* platform control, which is a downgrade in looks and not in function.
*
* That is the whole component: 0.84 kB against Radix's 31.5.
*/
export default function SelectDemo() {
const [value, setValue] = useState('medium')
return (
<>
<Select.Root value={value} onValueChange={setValue} aria-label="Size">
<Select.Trigger>
<Select.Value />
</Select.Trigger>
<Select.Item value="small">Small</Select.Item>
<Select.Item value="medium">Medium</Select.Item>
<Select.Item value="large">Large</Select.Item>
</Select.Root>
<output>value: {value}</output>
</>
)
}This is the largest single saving in the library: 0.84 kB against Radix's 31.5 kB, because almost all of it is an element that already exists.
Anatomy
<Select.Root>
<Select.Trigger>
<Select.Value />
</Select.Trigger>
<Select.Item />
<Select.Group />
<Select.Separator />
</Select.Root>
Select.Content exists and is optional — items may sit directly inside the
root, which is what a <select> expects.
Select.Root
Renders <select data-bedrock-select>. Takes every <select> prop.
| prop | type | notes |
|---|---|---|
value |
string |
Controlled. |
defaultValue |
string |
Uncontrolled. |
onValueChange |
(value: string) => void |
Alongside onChange. |
asChild |
boolean |
Supported. |
Because it is a <select>, name, required, disabled and form do what
they say, and the control submits with the form without a hidden input.
Select.Trigger / Select.Value
Trigger renders <button>; Value renders <selectedcontent>. Both are the
parts of the stylable-select markup that Chrome added — not bedrock inventions.
Select.Item
Renders <option>. Takes a required value.
Select.ItemText renders a <span> inside it. Select.ItemIndicator renders
nothing — style option::checkmark instead, and it warns in development if
you give it a className.
Select.Group / Select.Separator
<optgroup> and <hr>. Both are valid inside a <select> in the stylable
form.
Degrading
Where appearance: base-select is missing, the control falls back to the
platform select: the native dropdown, unstyled by your CSS.
That is a downgrade in looks, not in function — every option is still reachable, still typeahead-searchable, still submits. See browser support for the current state.
This is the one primitive where the visual gap between engines is large, and it is worth deciding deliberately whether that is acceptable for your product before adopting it. See gaps.
What is not here
- Arbitrary markup in an option. A
<select>restricts what may go inside it, and stylable select loosens that but does not remove it. - Multi-select as a separate component. Set
multipleon the root; it is a<select>. - A JavaScript-positioned listbox. The browser positions it, including on mobile where it is a native picker.