icon

Morainev0.5.0

MultiSelect
formsmulti-select

MultiSelect

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

Import#

import { MultiSelect } from 'moraine'

Slot Structure#

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

Control#

control
├── tagsContainer
│ ├── tag (×n, Badge)
│ ├── tagOverflow (optional)
│ └── input
├── clear (IconButton, optional)
└── trigger (IconButton)

Listbox#

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.

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.

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.

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.

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:

Terminal window
bun add @tanstack/virtual-core
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#

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

Data Attributes

5
Data AttributeDescription
data-disabled
Present when the component or item is disabled.
data-invalid
Present when the field has a validation error.
data-required
Present when the field is required.
data-size
Stores the resolved size variant.
data-variant
Stores the resolved visual variant.

Props#

PropTypeDefaultDescription
allowClearboolean | undefinedfalse
Show a clear button when a value is selected.
allowCreateboolean | undefined
Allow creating new tags on Enter when no match is found.
classClassValue
Class applied to the component root or trigger element.
classesMultiSelectT.Classes | undefined
closeIconIconT.Name
Icon used when the action button clears the selection. Tag remove buttons keep using this icon as well.
defaultOpenboolean | undefinedfalse
Initial open state.
defaultSearchValuestring | undefined
Default search value.
defaultValueTItem[] | undefined
The default value of the input (uncontrolled).
disabledboolean | undefinedfalse
Whether the input is disabled.
emptyRenderComponentOrElement<MultiSelectT.EmptyRenderProps<TItem>> | undefined
Custom renderer for the empty state when current filtered result has no matches.
filterOptionboolean | "startsWith" | "endsWith" | "contains" | ((inputValue: string, option: MultiSelectT.Item<TItem>) => boolean) | undefinedtrue
Filter function or boolean. `false` disables filtering.
gutternumber | undefined0
Gap (px) between the control and popup content.
idstring | undefined
The ID of the input element.
itemProps((option: MultiSelectT.Item<TItem> & BaseSelectT.OptionRenderState) => ElementProps<HTMLDivElement> | undefined) | undefined
Additional attributes for an option row.
labelRenderComponentOrElement<MultiSelectT.LabelRenderProps<TItem>> | undefined
Custom renderer for the option label text.
leadingIconIconT.Name
Icon shown before the input/value area.
listboxPropsElementProps<HTMLDivElement> | undefined
Additional attributes for the listbox element.
loadingboolean | undefined
Whether the select is in a loading state.
loadingIconIconT.Nameicon-loading
Icon shown during loading state.
maxCountnumber | undefined
Maximum number of selected values (multiple/tags).
maxTagCountnumber | undefined
Maximum visible tags before showing +N (visual only).
namestring | 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.
openboolean | undefined
Controlled open state.
optionRenderComponentOrElement<MultiSelectT.OptionRenderProps<TItem>> | undefined
Custom renderer for each option in the dropdown. Passes `null` for empty state.
optionsMultiSelectT.Item<TItem>[] | undefined
Available options.
overflowPaddingnumber | undefined4
Padding (px) used when calculating popup overflow and viewport collision.
placeholderstring | undefined
Placeholder text shown when no value is selected.
refJSX.HTMLElementTags["div"] extends { ref?: infer Ref; } ? Ref : never | undefined
requiredboolean | undefinedfalse
Whether the input is required.
scrollBottomThresholdnumber | undefined20
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.
searchboolean | undefinedfalse
Enable search input.
searchMaxLengthnumber | undefined
Maximum search text length applied on final commit.
searchValuestring | undefined
Controlled search value.
size"xs" | "sm" | "md" | "lg" | "xl" | undefined
styleJSX.CSSProperties | undefined
stylesMultiSelectT.Styles | undefined
tagRenderComponentOrElement<MultiSelectT.TagRenderProps<TItem>> | undefined
Custom renderer for each selected tag (multiple/tags).
tagVariant"default" | "outline" | "solid" | undefined
Variant for the selected tags.
tokenSeparatorsstring[] | undefined
Characters that split input into tokens and immediately select them.
trailingIconIconT.Nameicon-chevron-down
Icon used when the action button opens the dropdown.
valueTItem[] | undefined
The current value of the input (controlled).
variant"none" | "outline" | "ghost" | "subtle" | undefined
virtualRenderComponent<MultiSelectT.VirtualRenderProps<TItem>> | undefined
Renders flattened group labels and options through a virtualization layer.

Items#

PropTypeDefaultDescription
childrenOmit<BaseSelectT.Item<MultiSelectT.Value>, "children">[] | undefined
One-layer child options for grouped select.
descriptionstring | JSX.Element
Description shown below the label.
disabledboolean | undefined
Whether the option is disabled.
iconIconT.Name
Icon shown next to the label.
keystring | undefined
Text key used for filtering and matching; set this when `label` is not a string.
labelstring | JSX.Element
Label to display for the option, or the option group title.
valueMultiSelectT.Value | undefined
Value of the option.