Select

A custom dropdown, not a native select — the trigger matches Input exactly and the panel is real markup. Full keyboard support, type-ahead, and dismissal on Escape or outside click.

src/components/ui/select.tsx

About

From the component's own source, where it was written.

A custom dropdown, not a native <select>: the trigger matches Input exactly, and the panel is real markup that can nest.

An option with children becomes a submenu, and those nest without limit. Each level owns its own open state and highlight rather than flattening into one index — a flat model cannot express "the third item of the second submenu is focused while its parent row stays highlighted".

Install

npx astralyx-ui add select

Usage

import { Select } from '@/components/ui/select'

const options = [
  { value: 'system', label: 'System' },
  { value: 'light', label: 'Light' },
  { value: 'dark', label: 'Dark' },
]

<Select options={options} defaultValue="system" />

Composer

Change the props and the source below follows.

Props
tsx
<Select
  options={options}
  placeholder="Select a theme"
/>

Examples

Nested submenus

Variants

Sizes

With icon, and a disabled option

States

API

Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.

Select

Extends Omit<ComponentProps<'div'>, 'onChange' | 'defaultValue'>.

PropTypeDefaultDescription
errorbooleanfalseRed border. Also sets aria-invalid.
variant'default' | 'secondary' | 'ghost''default'Matches Input, so the two line up in a form.
size'sm' | 'default' | 'xs' | 'lg' | 'xl''default'Same size steps as Input.
optionsSelectOption[]requiredThe choices. A disabled option is skipped by arrow keys and type-ahead.
valuestring—Controlled and uncontrolled selection.
defaultValuestring—Controlled and uncontrolled selection.
onValueChange(value: string) => void—Fires with the chosen value.
placeholderstring'Select…'Shown until something is chosen.
disabledbooleanfalse—
iconReactNode—Leading adornment in the trigger.
triggerClassNamestring——
triggerLabelstring—Accessible name for the trigger.
emptyMessageReactNode'No options'Shown when options is empty.

Functions

FunctionSignatureDescription
selectVariants(props?: { variant?: 'default' | 'secondary' | 'ghost'; size?: 'sm' | 'default' | 'xs' | 'lg' | 'xl'; error?: boolean }) => string—

Types

SelectOption

A custom dropdown, not a native <select>: the trigger matches Input exactly, and the panel is real markup that can nest.

FieldTypeDescription
valuestring—
labelstring—
disabled?boolean—
children?SelectOption[]Makes an option a submenu instead of a value, and nests without limit. The row shows a chevron and cannot be selected itself.

Behaviour

TopicDetailDescription
placementright → left → topA submenu opens to the right, falls back to the left, then above. Fixed positioning, so it is never clipped by a scrolling ancestor.
keyboardarrowsRight or Enter opens a submenu and focuses it, Left closes one level and returns focus to the parent row, Escape closes. Type-ahead searches the level you are in.