---
title: Select
description: Dropdown select component with search and custom item rendering.
sidebar:
  order: 10
search:
  tags: [combobox, dropdown, options, searchable]
---

# Select

> Dropdown select component with search and custom item rendering.

## Import

```tsx
import { Select } from 'moraine'
```

## Slot Structure

Trigger control and a floating listbox with grouped options. The control or search input keeps focus while options provide selection, highlight, and active-descendant semantics.

### Control

```text
control
├── leading (Icon, optional)
├── input
├── clear (IconButton, optional)
└── trigger (IconButton)
```

### Listbox

```text
content (portal)
├── listbox
│   ├── item (×n)
│   │   ├── itemLabel
│   │   ├── itemDescription (optional)
│   │   └── itemTrailing (optional)
│   └── group (×n, optional)
│       └── label (optional)
└── empty (optional, no matches)
```

## Examples

### Single Select

Basic single selection with controlled value.

```tsx
function SingleSelect() {
  const FRUIT_OPTIONS: SelectT.Item[] = [
    { label: 'Apple', value: 'apple' },
    { label: 'Banana', value: 'banana' },
    { label: 'Cherry', value: 'cherry' },
    { label: 'Date', value: 'date' },
    { label: 'Elderberry', value: 'elderberry', disabled: true },
    { label: 'Forest', value: 'forest', icon: 'i-lucide:braces' },
  ]

  const [singleValue, setSingleValue] = createSignal<SelectT.Value | null>(null)

  return (
    <div class="w-80 space-y-2">
      <Select
        options={FRUIT_OPTIONS}
        value={singleValue()}
        onChange={setSingleValue}
        placeholder="Pick a fruit..."
      />
      <p class="text-xs text-muted-foreground">Selected: {singleValue() ?? 'none'}</p>
    </div>
  )
}
```

### Variants

Visual style variants.

```tsx
function Variants() {
  const FRUIT_OPTIONS: SelectT.Item[] = [
    { label: 'Apple', value: 'apple' },
    { label: 'Banana', value: 'banana' },
    { label: 'Cherry', value: 'cherry' },
    { label: 'Date', value: 'date' },
    { label: 'Elderberry', value: 'elderberry', disabled: true },
    { label: 'Forest', value: 'forest', icon: 'i-lucide:braces' },
  ]

  const VARIANTS = ['outline', 'subtle', 'ghost', 'none'] as const

  return (
    <div class="gap-3 grid w-80 sm:grid-cols-2">
      <For each={VARIANTS}>
        {(variant) => <Select options={FRUIT_OPTIONS} variant={variant} placeholder={variant} />}
      </For>
    </div>
  )
}
```

### Sizes

From xs to xl.

```tsx
function Sizes() {
  const FRUIT_OPTIONS: SelectT.Item[] = [
    { label: 'Apple', value: 'apple' },
    { label: 'Banana', value: 'banana' },
    { label: 'Cherry', value: 'cherry' },
    { label: 'Date', value: 'date' },
    { label: 'Elderberry', value: 'elderberry', disabled: true },
    { label: 'Forest', value: 'forest', icon: 'i-lucide:braces' },
  ]

  const SIZES = ['xs', 'sm', 'md', 'lg', 'xl'] as const

  return (
    <div class="gap-3 grid w-[42rem] md:grid-cols-5 sm:grid-cols-3">
      <For each={SIZES}>
        {(size) => <Select options={FRUIT_OPTIONS} size={size} placeholder={`Size: ${size}`} />}
      </For>
    </div>
  )
}
```

### Disabled

Non-interactive state.

```tsx
function Disabled() {
  const FRUIT_OPTIONS: SelectT.Item[] = [
    { label: 'Apple', value: 'apple' },
    { label: 'Banana', value: 'banana' },
    { label: 'Cherry', value: 'cherry' },
    { label: 'Date', value: 'date' },
    { label: 'Elderberry', value: 'elderberry', disabled: true },
    { label: 'Forest', value: 'forest', icon: 'i-lucide:braces' },
  ]

  return (
    <div class="w-80">
      <Select options={FRUIT_OPTIONS} disabled value="apple" placeholder="Pick..." />
    </div>
  )
}
```

### Searchable

Type to filter options.

```tsx
function Searchable() {
  const FRUIT_OPTIONS: SelectT.Item[] = [
    { label: 'Apple', value: 'apple' },
    { label: 'Banana', value: 'banana' },
    { label: 'Cherry', value: 'cherry' },
    { label: 'Date', value: 'date' },
    { label: 'Elderberry', value: 'elderberry', disabled: true },
    { label: 'Forest', value: 'forest', icon: 'i-lucide:braces' },
  ]

  return (
    <div class="w-80">
      <Select
        options={FRUIT_OPTIONS}
        search
        leadingIcon="i-lucide-search"
        placeholder="Search fruits..."
      />
    </div>
  )
}
```

### Grouped Options

Options organized in sections.

```tsx
function GroupedOptions() {
  const GROUPED_OPTIONS: SelectT.Item[] = [
    {
      label: 'Fruits',
      children: [
        { label: 'Apple', value: 'apple' },
        { label: 'Banana', value: 'banana' },
        { label: 'Cherry', value: 'cherry' },
      ],
    },
    {
      label: 'Vegetables',
      children: [
        { label: 'Carrot', value: 'carrot' },
        { label: 'Broccoli', value: 'broccoli' },
        { label: 'Spinach', value: 'spinach' },
      ],
    },
  ]

  return (
    <div class="w-80">
      <Select options={GROUPED_OPTIONS} placeholder="Pick an item..." />
    </div>
  )
}
```

### Infinite Scroll

Scroll to the bottom to load more options.

```tsx
function InfiniteScroll() {
  function makeOptions(count: number, offset = 0): SelectT.Item[] {
    return Array.from({ length: count }, (_, i) => ({
      label: `Option ${offset + i + 1}`,
      value: `opt-${offset + i + 1}`,
    }))
  }

  const [infiniteOptions, setInfiniteOptions] = createSignal<SelectT.Item[]>(makeOptions(20))

  const [loadingMore, setLoadingMore] = createSignal(false)

  return (
    <div class="w-80 space-y-2">
      <Select
        options={infiniteOptions()}
        classes={{
          listbox: 'max-h-100',
        }}
        onScrollBottom={() => {
          if (loadingMore()) {
            return
          }
          setLoadingMore(true)
          setTimeout(() => {
            const next = infiniteOptions().length
            setInfiniteOptions((prev) => [...prev, ...makeOptions(10, next)])
            setLoadingMore(false)
          }, 1000)
        }}
        scrollBottomThreshold={30}
        loading={loadingMore()}
        placeholder="Scroll to load more..."
      />
      <p class="text-xs text-muted-foreground">Total options: {infiniteOptions().length}</p>
    </div>
  )
}
```

## Virtual Rendering

Import `useListVirtualizer` from `moraine/utils` to render only visible entries. Pass its `virtualRender` to Select and forward `scrollToItem` to its `scrollToIndex` method so keyboard highlighting can reveal off-screen options.

Install the adapter's optional peer dependency before using it:

```bash
bun add @tanstack/virtual-core
```

```tsx
function Virtualization() {
  const virtualizer = useListVirtualizer<
    SelectT.VirtualEntry<string>,
    HTMLDivElement,
    HTMLDivElement
  >({
    estimateSize: (entry) => (entry.type === 'label' ? 30 : 32),
    getItemKey: (entry) => entry.key,
    overscan: 8,
  })

  return (
    <div class="w-80">
      <Select
        options={OPTIONS}
        placeholder="Pick one of 10,000 options..."
        virtualRender={virtualizer.virtualRender}
        scrollToItem={(_, entryIndex) => virtualizer.scrollToIndex(entryIndex)}
        classes={{ listbox: 'h-80 max-h-80' }}
      />
    </div>
  )
}
```

## API Reference

### Attributes

#### `root`

Select root that owns open state, value display, and popup positioning.

##### Data Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| data-disabled | string \| undefined | Present when the component or item is disabled. |
| data-invalid | string \| undefined | Present when the field has a validation error. |
| data-required | string \| undefined | Present when the field is required. |

#### `content`

Popup panel that contains search input, options, groups, and empty state.

##### CSS Variables

| Attribute | Type | Description |
| --- | --- | --- |
| --mo-popper-content-transform-origin | string | CSS custom property exposed by this slot. |

#### `listbox`

ARIA listbox that contains selectable options.

##### ARIA Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| aria-multiselectable | boolean \| string \| undefined | Accessibility attribute forwarded by the rendered component. |
| role | string | Defines the semantic role exposed to assistive technology. |

#### `item`

Selectable option row inside the listbox.

##### Data Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| data-disabled | string \| undefined | Present when the component or item is disabled. |
| data-highlighted | string \| undefined | Present when the item is highlighted by pointer or keyboard navigation. |
| data-selected | string \| undefined | Present when the item is selected. |

##### ARIA Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| aria-disabled | boolean \| string \| undefined | Indicates that the control is disabled. |
| aria-posinset | boolean \| string \| undefined | Accessibility attribute forwarded by the rendered component. |
| aria-selected | boolean \| string \| undefined | Indicates the currently selected option or tab. |
| aria-setsize | boolean \| string \| undefined | Accessibility attribute forwarded by the rendered component. |
| role | string | Defines the semantic role exposed to assistive technology. |

#### `group`

Option group wrapper inside the listbox.

##### ARIA Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| role | string | Defines the semantic role exposed to assistive technology. |

#### `label`

Group label or option label text, depending on context.

#### `control`

Closed select control that displays the current value and opens the popup.

##### Data Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| data-disabled | string \| undefined | Present when the component or item is disabled. |
| data-invalid | string \| undefined | Present when the field has a validation error. |
| data-required | string \| undefined | Present when the field is required. |

#### `input`

Search input or value text field inside the control.

#### `leading`

Icon shown before the select input or value.

#### `trigger`

Button region that toggles the select popup.

##### Data Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| data-loading | string \| undefined | Present when the component is loading. |

#### `empty`

Message shown when filtering leaves no selectable options.

#### `itemLabel`

Primary label text inside an option row.

#### `itemDescription`

Supporting description text inside an option row.

#### `itemTrailing`

Trailing region inside an option row, usually for selection state or custom content.

### Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| class | ClassValue | — | Class applied to the component root or trigger element. |
| classes | SelectT.Classes \| undefined | — | — |
| closeIcon | IconT.Name | — | Icon kept for API compatibility; Select has no clear action. |
| defaultOpen | boolean \| undefined | false | Initial open state. |
| defaultSearchValue | string \| undefined |  | Default search value. |
| defaultValue | TItem \| null \| undefined | — | The default value of the input (uncontrolled). |
| disabled | boolean \| undefined | false | Whether the input is disabled. |
| emptyRender | ComponentOrElement<SelectT.EmptyRenderProps<TItem>> \| undefined | — | Custom renderer for the empty state when current filtered result has no matches. |
| filterOption | boolean \| "startsWith" \| "endsWith" \| "contains" \| ((inputValue: string, option: SelectT.Item<TItem>) => boolean) \| undefined | true | Filter function or boolean. `false` disables filtering. |
| gutter | number \| undefined | 0 | Gap (px) between the control and popup content. |
| id | string \| undefined | — | The ID of the input element. |
| itemProps | ((option: SelectT.Item<TItem> & BaseSelectT.OptionRenderState) => ElementProps<HTMLDivElement> \| undefined) \| undefined | — | Additional attributes for an option row. |
| labelRender | ComponentOrElement<SelectT.LabelRenderProps<TItem>> \| undefined | — | Custom renderer for the option label text. |
| leadingIcon | IconT.Name | — | Icon shown before the input/value area. |
| listboxProps | ElementProps<HTMLDivElement> \| undefined | — | Additional attributes for the listbox element. |
| loading | boolean \| undefined | — | Whether the select is in a loading state. |
| loadingIcon | IconT.Name | icon-loading | Icon shown during loading state. |
| name | string \| undefined | — | The name of the input element, used for form submission. |
| onChange | ((value: NoInfer<TItem \| null>) => void) \| undefined | — | Called when the selection changes. |
| onOpenChange | ((open: boolean) => void) \| undefined | — | Called whenever the popup open state changes. |
| onScrollBottom | (() => void) \| undefined | — | Called when the listbox is scrolled to bottom. Useful for infinite loading scenarios. Make sure to set `overflowPadding` and `scrollBottomThreshold` appropriately to ensure the callback is triggered at the right time. |
| onSearch | ((value: string) => void) \| undefined | — | Called when the search input changes. |
| open | boolean \| undefined | — | Controlled open state. |
| optionRender | ComponentOrElement<SelectT.OptionRenderProps<TItem>> \| undefined | — | Custom renderer for each option in the dropdown. Passes `null` for empty state. |
| options | SelectT.Item<TItem>[] \| undefined | — | Available options. |
| overflowPadding | number \| undefined | 4 | Padding (px) used when calculating popup overflow and viewport collision. |
| placeholder | string \| undefined |  | Placeholder text shown when no value is selected. |
| ref | JSX.HTMLElementTags["div"] extends { ref?: infer Ref; } ? Ref : never \| undefined | — | — |
| required | boolean \| undefined | false | Whether the input is required. |
| scrollBottomThreshold | number \| undefined | 20 | Distance (px) from the bottom at which onScrollBottom fires. |
| scrollToItem | ((item: SelectT.Item<TItem>, entryIndex: number) => void) \| undefined | — | Scrolls a highlighted option into view using its flattened entry index. |
| search | boolean \| undefined | false | Enable search input. |
| searchMaxLength | number \| undefined | — | Maximum search text length applied on final commit. |
| searchValue | string \| undefined | — | Controlled search value. |
| size | "xs" \| "sm" \| "md" \| "lg" \| "xl" \| undefined | — | — |
| style | JSX.CSSProperties \| undefined | — | — |
| styles | SelectT.Styles \| undefined | — | — |
| trailingIcon | IconT.Name | icon-chevron-down | Icon for the dropdown trigger. |
| value | TItem \| null \| undefined | — | The current value of the input (controlled). |
| variant | "none" \| "outline" \| "ghost" \| "subtle" \| undefined | — | — |
| virtualRender | Component<SelectT.VirtualRenderProps<TItem>> \| undefined | — | Renders flattened group labels and options through a virtualization layer. |

### Items

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| children | Omit<BaseSelectT.Item<SelectT.Value>, "children">[] \| undefined | — | One-layer child options for grouped select. |
| description | string \| JSX.Element | — | Description shown below the label. |
| disabled | boolean \| undefined | — | Whether the option is disabled. |
| icon | IconT.Name | — | Icon shown next to the label. |
| key | string \| undefined | — | Text key used for filtering and matching; set this when `label` is not a string. |
| label | string \| JSX.Element | — | Label to display for the option, or the option group title. |
| value | SelectT.Value \| undefined | — | Value of the option. |

### ARIA

Accessibility attributes and roles emitted by the component markup.

| Attribute | Type | Description |
| --- | --- | --- |
| aria-disabled | boolean \| string \| undefined | Indicates that the control is disabled. |
| aria-hidden | boolean \| string \| undefined | Hides decorative content from assistive technology. |
| aria-multiselectable | boolean \| string \| undefined | Accessibility attribute forwarded by the rendered component. |
| aria-posinset | boolean \| string \| undefined | Accessibility attribute forwarded by the rendered component. |
| aria-selected | boolean \| string \| undefined | Indicates the currently selected option or tab. |
| aria-setsize | boolean \| string \| undefined | Accessibility attribute forwarded by the rendered component. |
| role | string | Defines the semantic role exposed to assistive technology. |

### Data Attributes

State and slot attributes exposed for styling hooks and selectors.

| Attribute | Type | Description |
| --- | --- | --- |
| data-disabled | string \| undefined | Present when the component or item is disabled. |
| data-empty-option | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |
| data-highlighted | string \| undefined | Present when the item is highlighted by pointer or keyboard navigation. |
| data-invalid | string \| undefined | Present when the field has a validation error. |
| data-loading | string \| undefined | Present when the component is loading. |
| data-required | string \| undefined | Present when the field is required. |
| data-selected | string \| undefined | Present when the item is selected. |
| data-slot | string | Identifies the rendered slot for styling hooks and selectors. |
