---
title: MultiSelect
description: Dropdown select component with search, multi-select, and custom item rendering.
sidebar:
  order: 11
search:
  tags: [combobox, multiple selection, tags, options]
---

# MultiSelect

> Dropdown select component with search, multi-select, and custom item rendering.

## Import

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

## Slot Structure

Tag container with inline input and a floating listbox with grouped options.

### Control

```text
control
├── tagsContainer
│   ├── tag (×n, Badge)
│   ├── tagOverflow (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

### Multiple Select

Multiple selection with tags and `allowClear`.

```tsx
function MultipleSelect() {
  const FRUIT_OPTIONS: MultiSelectT.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 [multiValue, setMultiValue] = createSignal<MultiSelectT.Value[]>([])

  return (
    <div class="w-80 space-y-2">
      <MultiSelect
        options={FRUIT_OPTIONS}
        value={multiValue()}
        onChange={setMultiValue}
        placeholder="Pick fruits..."
        allowClear
        classes={{ control: 'w-full' }}
      />
      <p class="text-xs text-muted-foreground">Selected: {multiValue().join(', ') || 'none'}</p>
    </div>
  )
}
```

### Token Separators

Create and select tags when a separator is typed.

```tsx
function TokenSeparators() {
  const FRUIT_OPTIONS: MultiSelectT.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 [tagValues, setTagValues] = createSignal<MultiSelectT.Value[]>([])

  return (
    <div class="w-80 space-y-2">
      <MultiSelect
        search
        options={FRUIT_OPTIONS}
        value={tagValues()}
        onChange={setTagValues}
        tokenSeparators={[' ']}
        placeholder="Type text and press Space..."
      />
      <p class="text-xs text-muted-foreground">Tags: {tagValues().join(', ') || 'none'}</p>
    </div>
  )
}
```

### Create New Tags

Type a new value and press Enter or click Create in the empty state.

```tsx
function CreateNewTags() {
  const FRUIT_OPTIONS: MultiSelectT.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 [createTagValues, setCreateTagValues] = createSignal<MultiSelectT.Value[]>([])

  return (
    <div class="w-80 space-y-2">
      <MultiSelect
        search
        loading
        options={FRUIT_OPTIONS}
        value={createTagValues()}
        onChange={setCreateTagValues}
        allowCreate
        placeholder="Type to create tags..."
        emptyRender={(ctx) => (
          <div class="p-2 text-center">
            <Button variant="link" size="sm" class="text-primary" onClick={() => ctx.create()}>
              Create &ldquo;{ctx.inputValue}&rdquo;
            </Button>
          </div>
        )}
      />
      <p class="text-xs text-muted-foreground">Tags: {createTagValues().join(', ') || 'none'}</p>
    </div>
  )
}
```

### Max Count & Max Tag Count

Limit selections and visible chips.

```tsx
function MaxCountMaxTagCount() {
  const FRUIT_OPTIONS: MultiSelectT.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="gap-4 grid w-[42rem] sm:grid-cols-2">
      <div class="space-y-1">
        <label class="text-xs text-muted-foreground block">maxCount=2</label>
        <MultiSelect options={FRUIT_OPTIONS} maxCount={2} placeholder="Pick up to 2..." />
      </div>
      <div class="space-y-1">
        <label class="text-xs text-muted-foreground block">maxTagCount=1 (value has 3)</label>
        <MultiSelect
          options={FRUIT_OPTIONS}
          defaultValue={['apple', 'banana', 'cherry']}
          maxTagCount={1}
          placeholder="Pick..."
        />
      </div>
    </div>
  )
}
```

## Virtual Rendering

Import `useListVirtualizer` from `moraine/utils` to render only visible entries. Pass its `virtualRender` to MultiSelect 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<
    MultiSelectT.VirtualEntry<string>,
    HTMLDivElement,
    HTMLDivElement
  >({
    estimateSize: (entry) => (entry.type === 'label' ? 30 : 32),
    getItemKey: (entry) => entry.key,
    overscan: 8,
  })

  return (
    <div class="w-80">
      <MultiSelect
        options={OPTIONS}
        placeholder="Pick from 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. |
| data-size | string | Stores the resolved size variant. |
| data-variant | string | Stores the resolved visual variant. |

#### `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`

Multi-select control that displays selected tags 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 used to filter or add selections.

#### `leading`

Icon shown before the selected tags and search input.

#### `trigger`

Button region that toggles the multi-select popup.

#### `clear`

Button used to clear all selected values.

#### `tagsContainer`

Wrapper that lays out selected value tags inside the control.

#### `tag`

Badge representing one selected value.

#### `tagRemove`

Button used to remove one selected value.

#### `tagOverflow`

Counter shown when selected tags exceed the visible limit.

#### `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 |
| --- | --- | --- | --- |
| allowClear | boolean \| undefined | false | Show a clear button when a value is selected. |
| allowCreate | boolean \| undefined | — | Allow creating new tags on Enter when no match is found. |
| class | ClassValue | — | Class applied to the component root or trigger element. |
| classes | MultiSelectT.Classes \| undefined | — | — |
| closeIcon | IconT.Name | — | Icon used when the action button clears the selection.<br>Tag remove buttons keep using this icon as well. |
| defaultOpen | boolean \| undefined | false | Initial open state. |
| defaultSearchValue | string \| undefined |  | Default search value. |
| defaultValue | TItem[] \| undefined | — | The default value of the input (uncontrolled). |
| disabled | boolean \| undefined | false | Whether the input is disabled. |
| emptyRender | ComponentOrElement<MultiSelectT.EmptyRenderProps<TItem>> \| undefined | — | Custom renderer for the empty state when current filtered result has no matches. |
| filterOption | boolean \| "startsWith" \| "endsWith" \| "contains" \| ((inputValue: string, option: MultiSelectT.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: MultiSelectT.Item<TItem> & BaseSelectT.OptionRenderState) => ElementProps<HTMLDivElement> \| undefined) \| undefined | — | Additional attributes for an option row. |
| labelRender | ComponentOrElement<MultiSelectT.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. |
| maxCount | number \| undefined | — | Maximum number of selected values (multiple/tags). |
| maxTagCount | number \| undefined | — | Maximum visible tags before showing +N (visual only). |
| name | string \| undefined | — | The name of the input element, used for form submission. |
| onChange | ((value: NoInfer<TItem[]>) => void) \| undefined | — | Called when the selection changes. |
| onClear | (() => void) \| undefined | — | Called when clear is triggered. |
| 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<MultiSelectT.OptionRenderProps<TItem>> \| undefined | — | Custom renderer for each option in the dropdown. Passes `null` for empty state. |
| options | MultiSelectT.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: MultiSelectT.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 | MultiSelectT.Styles \| undefined | — | — |
| tagRender | ComponentOrElement<MultiSelectT.TagRenderProps<TItem>> \| undefined | — | Custom renderer for each selected tag (multiple/tags). |
| tagVariant | "default" \| "outline" \| "solid" \| undefined | — | Variant for the selected tags. |
| tokenSeparators | string[] \| undefined | — | Characters that split input into tokens and immediately select them. |
| trailingIcon | IconT.Name | icon-chevron-down | Icon used when the action button opens the dropdown. |
| value | TItem[] \| undefined | — | The current value of the input (controlled). |
| variant | "none" \| "outline" \| "ghost" \| "subtle" \| undefined | — | — |
| virtualRender | Component<MultiSelectT.VirtualRenderProps<TItem>> \| undefined | — | Renders flattened group labels and options through a virtualization layer. |

### Items

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| children | Omit<BaseSelectT.Item<MultiSelectT.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 | MultiSelectT.Value \| undefined | — | Value of the option. |

### ARIA

Accessibility attributes and roles emitted by the component markup.

| Attribute | Type | Description |
| --- | --- | --- |
| aria-busy | boolean \| string \| undefined | Accessibility attribute forwarded by the rendered component. |
| aria-disabled | boolean \| string \| undefined | Indicates that the control is disabled. |
| aria-hidden | boolean \| string \| undefined | Hides decorative content from assistive technology. |
| aria-label | boolean \| string \| undefined | Provides an accessible label when visible text is not sufficient. |
| 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-size | string | Stores the resolved size variant. |
| data-slot | string | Identifies the rendered slot for styling hooks and selectors. |
| data-variant | string | Stores the resolved visual variant. |
