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'
Loading demo…
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