Rime

Select

A custom listbox: a recessed field or filter chip that opens a frosted popover, optionally searchable.

Select replaces the native <select> with a combobox and a listbox popover that keeps the design system's look while matching native keyboard behaviour. It has two appearances: field (a grey frosted recess, so a choice reads as something you press rather than type in) and chip (the compact filter dropdown used in a FilterBar).

Options can be grouped (consecutive options sharing group), described (a second line), disabled, and counted (filter dropdowns show how many rows each option would show). searchable adds a pinned search well with keyword matching for long lists.

The popover portals to document.body, flips above the trigger when there is no room below, and returns focus to the trigger on close.

Installation

npx shadcn@latest add KezLahd/rime/select

Also installs: base, field, icon, search-field, text-input.

Usage

import { Field, Select } from "@/components/ui";

<Field label="Topic">
  <Select
    value={topic}
    onChange={setTopic}
    placeholder="Choose a topic"
    options={[
      { value: "billing", label: "Billing and invoices" },
      { value: "access", label: "Account access" },
    ]}
  />
</Field>

Examples

Open

inline renders the list open under its trigger, in place: how it looks when someone opens it.
Billing and invoices
Account access
Delivery details
Something else

Field appearance

Chip appearance (filters)

active marks a chip whose filter is applied; count shows the rows each option would leave.

Searchable, grouped

Invalid and disabled

Choose a reason.

API

Generated from the component's TypeScript types and JSDoc (scripts/gen-ui-registry.mjs), so it always matches the source.

Select

Select props
PropTypeDefaultDescription
optionsRequiredReadonlyArray<SelectOption<V>>
valueRequiredV | null
onChangeRequired(value: V) => void
placeholderstring"Select…"
appearance"field" | "chip""field"field = bordered form control. chip = the reference recessed filter-bar chip.
size"sm" | "md""md"
disabledboolean
invalidboolean
requiredboolean
idstring
namestringEmits a hidden input so the value posts with a native form.
activebooleanChip only: shows the "filter applied" tint. Defaults to value !== null.
leadingIconReactNode
classNamestring
searchablebooleanfalseA search box pinned at the top of the list. Typing filters by label, description, group and keywords, and highlights the match. For long lists (countries, cities, time zones); a short list reads faster without.
searchPlaceholderstring"Search"Placeholder, and accessible name, of the search box.
aria-labelstring
aria-labelledbystring
aria-describedbystring
onBlur() => void
inlinebooleanfalseRender the list open, in place under the trigger: no portal, no focus move, no outside-click listener, no scrolling into view. For docs and thumbnails.

Theme tokens

Per-component tokens are undeclared by default, so the default shown is the fallback the CSS uses. Set one at :root (or in theme.css) to restyle every instance. Live values are read from this page, so they follow the Rime Studio.

Select per-component tokens
TokenDefaultLive value
--chip-heightvar(--control-h-sm)…
--chip-radiusvar(--r-sm)…
--field-focus-borderrgba(var(--rgb-brand), 0.7) / var(--focus-color)…
--field-focus-ringvar(--focus-halo)…
--field-focus-ring-invalid0 0 0 4px rgba(var(--rgb-danger), 0.14)…
--field-hover-borderrgba(var(--rgb-brand-deep), 0.3)…
--input-bgvar(--surface-solid)…
--input-bordervar(--line-input)…
--input-border-widthvar(--border-w-field)…
--input-font-sizevar(--text-input)…
--input-height-mdvar(--field-h-md)…
--input-height-smvar(--field-h-sm)…
--input-pad-xcalc(14px * var(--density))…
--input-radiusvar(--r-md) / var(--r-sm)…
--input-shadowvar(--shadow-input)…
--popover-item-active-bglinear-gradient(to right, rgba(var(--rgb-brand), 0.14), rgba(var(--rgb-brand-deep), 0.07))…
--popover-item-radiusvar(--r-xs)…
--popover-padcalc(6px * var(--density))…
--popover-radiusvar(--r-lg)…
--recess-fill-disabledrgba(var(--rgb-white), 0.3)…
--recess-line-disabledrgba(var(--rgb-brand-deep), 0.1)…

System tokens it reads

Select system tokens
TokenDefaultLive value
--blur-recesstokens.css…
--border-wtokens.css…
--border-w-fieldtokens.css…
--brandtokens.css…
--brand-deeptokens.css…
--chip-active-filltokens.css…
--chip-active-fill-hovertokens.css…
--chip-active-shadowtokens.css…
--control-h-smtokens.css…
--danger-basetokens.css…
--danger-field-filltokens.css…
--densitytokens.css…
--durtokens.css…
--dur-fasttokens.css…
--easetokens.css…
--ease-outtokens.css…
--field-h-mdtokens.css…
--field-h-smtokens.css…
--focus-colortokens.css…
--focus-halotokens.css…
--focus-ringtokens.css…
--font-bodytokens.css…
--font-monotokens.css…
--ink-bodytokens.css…
--ink-brandtokens.css…
--ink-disabledtokens.css…
--ink-mutedtokens.css…
--ink-placeholdertokens.css…
--ink-secondarytokens.css…
--line-inputtokens.css…
--line-softtokens.css…
--popover-blurtokens.css…
--popover-bordertokens.css…
--popover-shadowtokens.css…
--popover-surfacetokens.css…
--r-2xstokens.css…
--r-lgtokens.css…
--r-mdtokens.css…
--r-smtokens.css…
--r-xstokens.css…
--recess-filltokens.css…
--recess-fill-hovertokens.css…
--rgb-brandtokens.css…
--rgb-brand-deeptokens.css…
--rgb-contacttokens.css…
--rgb-dangertokens.css…
--rgb-whitetokens.css…
--shadow-inputtokens.css…
--shadow-recesstokens.css…
--surface-solidtokens.css…
--surface-sunkentokens.css…
--text-bodytokens.css…
--text-captiontokens.css…
--text-inputtokens.css…
--text-microtokens.css…
--text-smalltokens.css…
--tracking-labeltokens.css…
--wash-activetokens.css…
--wash-strongtokens.css…
--wash-subtletokens.css…
--z-popovertokens.css…

Accessibility

  • The trigger is role="combobox" with aria-expanded, aria-controls and aria-activedescendant; options are role="option" with aria-selected.
  • Keyboard: Down, Up, Enter, Space, Home and End open it; Up/Down/Home/End/PageUp/PageDown move; typing jumps to the next label starting with those letters (closed, it selects); Enter or Space selects; Esc clears a search first, then closes, and stops there so an enclosing Modal stays open; Tab closes without selecting and moves on.
  • Searchable lists announce the result count through a polite status region.
  • A chip has no visible label, so aria-label is required on it.
  • Wrap it in <Field label=…>: Field wires the label, hint and error to the control (id, aria-describedby, aria-invalid) through context, so the control needs no ids of its own.

Do and don't

Do

  • Use appearance="chip" inside FilterBar, and "field" in forms.
  • Turn on searchable past roughly 12 options.
  • Use RadioGroup instead when there are 2 to 4 options a user should compare at a glance.

Don't

  • Don't use a native <select>; it cannot take the theme.
  • Don't put actions ("Add new…") inside the options; that is a Menu.

For AI agents

This page as markdown, rendered from the same registry entry. Point an agent at it (or add .md to this page's URL), or at /llms.txt for the index.

Open select.md

Related: text-input, filter-bar, radio-group, menu