Inputs

Select

A button-triggered select whose value crossfades on change, with an anchored list that flips at edges, groups and separators, typeahead and arrow-key navigation, and a sliding highlight.

A button-triggered select that takes its options as a data array. When the value changes, the trigger text crossfades; the list is portaled and anchored next to the trigger, flips to the opposite side and shifts along the edge when space runs out, and shows scroll hints when it gets long. It supports groups and separators, disabled options, typeahead, and arrow / Home / End navigation, with a highlight background that slides between options. Both the value and the open state can be controlled or uncontrolled, and passing name makes it submit with a native form.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add select.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

100
Active edited usageDefault props
<Select/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add https://webberui.com/r/select.en.json

Or, once registries are configured in components.json, install it as @webberui/select.

Usage

import { Select } from "@/components/ui/select";

const [language, setLanguage] = React.useState("en");

<Select
  aria-label="Interface language"
  value={language}
  onValueChange={setLanguage}
  options={[
    { value: "zh-TW", label: "繁體中文(台灣)" },
    { value: "en", label: "English" },
    { value: "ja", label: "日本語" },
  ]}
/>

// Grouped: a separator is inserted between groups automatically
<Select
  aria-label="Time zone"
  name="timezone"
  placeholder="Choose a time zone"
  groups={[
    { label: "Asia", options: [{ value: "Asia/Taipei", label: "Taipei (UTC+8)" }] },
    { label: "Europe", options: [{ value: "Europe/London", label: "London (UTC+0)" }] },
  ]}
/>

Props

PropTypeDefaultDescription
optionsSelectOption[]—Flat option list; use this or groups — groups wins when both are given
groupsSelectGroup[]—Grouped options; a separator is inserted between groups automatically
valuestring—Controlled value; when omitted the component manages it internally (uncontrolled)
defaultValuestring—Initial value in uncontrolled mode
onValueChange(value: string) => void—Called when the selected value changes
openboolean—Controlled open state
defaultOpenbooleanfalseInitial open state in uncontrolled mode
onOpenChange(open: boolean) => void—Called when the open state changes
placeholderstring"Select an option"Placeholder text shown when nothing is selected (the Chinese registry version defaults to "請選擇", "please choose")
disabledbooleanfalseDisable the whole select
namestring—Form field name; when set, a hidden input is rendered so native form submission includes the current value
size"sm" | "md""md"Trigger size
durationnumber100Enter/exit duration of the content (ms); also written to --wb-select-duration for CSS transitions
side"top" | "right" | "bottom" | "left""bottom"Preferred side for the content; flips to the opposite side when there is not enough room
align"start" | "center" | "end""start"Alignment of the content on the cross axis
sideOffsetnumber4Distance between the content and the trigger (px)
aria-labelstring—Accessible name for the trigger and the list
triggerClassNamestring—Extra classes applied to the trigger
classNamestring—Forwarded to the root element

SelectOption

FieldTypeDefaultDescription
valuestring—Option value (submitted with forms and passed to onValueChange)
labelstring—Display text; also what typeahead matches against
iconReactNode—Icon shown before the text
disabledbooleanfalseDisable this option: it cannot be selected and arrow keys / typeahead skip it
groupstring—Group name; only applies when using options — options with the same name are collected into one group

SelectGroup

FieldTypeDefaultDescription
labelstring—Group heading
optionsSelectOption[]—Options in the group

How it works

  • Positioning: the content is portaled to body with position: fixed and placed outside the trigger according to side / align / sideOffset. When the main axis overflows and the opposite side has more room it flips; on the cross axis it shifts back into the viewport (8px from the edge). It recomputes when the trigger or content resizes, the window resizes, or any ancestor scrolls, at most once per frame. The content carries data-side / data-align so you can style by actual placement.
  • Sizing: the width is the larger of the trigger width and the content width; the height is capped at 18rem and at the available viewport space. Beyond that the list scrolls, scroll hints appear at the top / bottom, and hovering a hint keeps scrolling.
  • Animation: on enter the content fades in, scales up from 0.95 and slides 8px in from against its side, with the transform origin set from the flipped, actual placement; closing reverses the same values. duration is also written to the CSS variable --wb-select-duration, which CSS transitions such as the chevron rotation read, so you can override it globally. The trigger text crossfades at 1.5× the duration when the value changes.
  • Typeahead: typing within 500ms accumulates a prefix that matches the start of option labels; CJK labels match on the first character, and repeating one character cycles through options with that initial. Typing while the trigger is focused but closed changes the value directly, like a native select; keys pressed during IME composition are ignored.
  • Passing name renders an <input type="hidden">, so a native form submission includes the current value.

Accessibility

  • The trigger is role="combobox" with aria-haspopup="listbox", and points to the list with aria-expanded / aria-controls; provide aria-label as its name
  • The list is role="listbox", takes focus when opened and points to the highlighted option with aria-activedescendant; options are role="option" with aria-selected and aria-disabled; groups are role="group" labelled by their heading via aria-labelledby
  • Keyboard: Enter, Space, ↑ and ↓ open the list; ↑ / ↓ move the highlight (skipping disabled options, no wrapping), Home / End jump to the ends; Enter / Space select and close, Tab selects the highlighted option and moves on, Escape closes without changing the value; focus returns to the trigger after closing
  • Clicking anywhere outside the content closes it
  • When the user has enabled reduce motion, the content and the value change only fade, with no scaling or sliding, and the highlight jumps straight to its new position