MenuChevron Down
Select - Docs - Artefact

Select

Forms
Auto-interactive

Introduction

A dropdown control for picking one or more options from a list — an accessible, styleable alternative to the native <select> element.

When to reach for something else:

  • With fewer than ~5 options, a RadioGroup is usually clearer.
  • If the user should be able to type to filter the options, use Combobox.

The component renders a visually hidden native <select> alongside the custom UI, so a name prop makes it participate in regular form submission with no extra wiring.

Keyboard Interaction

The trigger is a role="combobox" button; focus stays on it while the highlighted option is reported through aria-activedescendant.

KeyBehavior
Enter / SpaceOpen the list; when open, select the highlighted option.
ArrowDown / ArrowUpOpen the list, or move the highlight (wraps, skips disabled options).
Home / EndHighlight the first / last enabled option.
EscapeClose the list.
TabClose the list and move focus on.
Printable charactersTypeahead: jumps to the first option whose label matches the typed prefix. When the list is closed (single mode), the match is selected directly, like a native <select>.

Opening the list highlights the currently selected option (or the first enabled one), and keyboard navigation keeps the highlighted option scrolled into view.

Hydration

Tier 1 — auto-interactive. Opening the dropdown and selecting an option require client JS, and there is no static fallback (the native <select> is visually hidden and exists only for form submission), so Select hydrates by default. Pass interactive={false} to force a purely static render.

interactive propResult
omittedHydrates as an island
trueHydrates as an island
falseStatic — no client JS

All interactivity decisions in the library route through the shared shouldHydrate() helper in app/components/ui/island-utils.ts.

Usage

React
Solid
Svelte
Vue
Hono
import { Select } from "../components/ui";

const items = [
  { label: "React", value: "react" },
  { label: "Solid", value: "solid" },
  { label: "Svelte", value: "svelte", disabled: true },
  { label: "Vue", value: "vue" },
  { label: "Hono", value: "hono" },
];

export default function MyPage() {
  return (
    <Select
      items={items}
      label="Framework"
      placeholder="Select a framework"
      allowClear
    />
  );
}

Multiple selection

The list stays open while options are toggled, and the trigger shows the selected labels joined with commas.

<Select
  multiple
  items={items}
  label="Frameworks"
  placeholder="Select frameworks"
  defaultValue={["hono"]}
/>

In a form

The hidden native <select> carries the selection, so a plain form post works:

<form method="post" action="/frameworks">
  <Select name="framework" items={items} label="Framework" required />
  <Button type="submit">Save</Button>
</form>

Sizes and variants

<Select items={items} size="sm" placeholder="Small" />
<Select items={items} size="lg" variant="surface" placeholder="Large surface" />
<Select items={items} invalid placeholder="Invalid state" />

CMS Page Builder

This component is available as a select block in the Page Builder (content/pages/*.json):

{
  "type": "select",
  "label": "Framework",
  "placeholder": "Select a framework",
  "items": [
    { "label": "React", "value": "react" },
    { "label": "Hono", "value": "hono" }
  ]
}

Props

PropTypeDescription
itemsSelectItem[]The options to display in the list.
labelChildLabel rendered above the trigger and associated with it.
placeholderstringText shown in the trigger while nothing is selected.
allowClearbooleanShows a clear button once a selection exists.
multiplebooleanAllows selecting several options; the list stays open while toggling.
defaultValuestring[]Initial selection (uncontrolled). Alias of selectedValues.
selectedValuesstring[]Initial selection (same as defaultValue).
deselectablebooleanIn single mode, clicking the selected option again clears it.
namestringName of the hidden native <select>, for form submission.
disabledbooleanDisables the whole control.
invalidbooleanMarks the control invalid (aria-invalid, error border).
readOnlybooleanSelection is visible but the list cannot be opened.
requiredbooleanMarks the control required (aria-required, hidden select required).
openbooleanControlled open state of the dropdown.

| size | `"xs" \ | "sm" \ | "md" \ | "lg" \ | "xl"` | Size of the trigger and list. Defaults to md. |

| variant | `"outline" \ | "surface"` | Visual variant of the trigger. Defaults to outline. |

| interactive | boolean | Overrides the hydration decision (see below). | | onValueChange | (values: string[]) => void | Called with the full selection whenever it changes (select, deselect, clear). | | onItemSelect | (value: string) => void | Called with the value of the option that was interacted with. | | onClear | () => void | Called when the clear button empties the selection. | | onOpenChange | (open: boolean) => void | Called when the dropdown opens or closes. |

Callback props only work when the Select is composed from client-side code (inside another island). Props serialised from a server-rendered route must be plain data.

SelectItem

PropTypeDescription
labelstringThe display text for the option. Also used for typeahead matching.
valuestringThe unique value for the option.
disabledbooleanWhether the option can be selected.