Skip to content

Examples

Every demo below runs the code under it.

value: []

Basic.tsx
import { useState } from 'react'
import MultiSelect from 'react-multiple-select-dropdown-lite'
import 'react-multiple-select-dropdown-lite/dist/index.css'
const options = [
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' },
{ label: 'Cherry', value: 'cherry' },
{ label: 'Mango', value: 'mango' },
{ label: 'Orange', value: 'orange' }
]
export default function Basic() {
const [value, setValue] = useState<string[]>([])
return (
<>
<MultiSelect options={options} onChange={setValue} />
<p className='demo-output'>value: {JSON.stringify(value)}</p>
</>
)
}

singleSelect keeps one value and closes the menu after a pick. Disabled options stay visible but can’t be picked.

value: ["m"]

SingleSelect.tsx
import { useState } from 'react'
import MultiSelect from 'react-multiple-select-dropdown-lite'
import 'react-multiple-select-dropdown-lite/dist/index.css'
const options = [
{ label: 'Small', value: 's' },
{ label: 'Medium', value: 'm' },
{ label: 'Large', value: 'l' },
{ label: 'Extra large (sold out)', value: 'xl', disabled: true }
]
export default function SingleSelect() {
const [value, setValue] = useState(['m'])
return (
<>
<MultiSelect
singleSelect
options={options}
defaultValue={['m']}
onChange={setValue}
placeholder='Pick a size'
/>
<p className='demo-output'>value: {JSON.stringify(value)}</p>
</>
)
}

Groups have type: 'group', a title and childs, and can nest. Options themselves can’t have type: 'group' or type: 'submenu'. emptyDataLabel shows when a group has no options. With collapsible: true, a click on the title (or Enter) shows and hides the group’s options; collapsed: true starts it closed. Search looks inside groups, closed ones too.

Groups.tsx
import MultiSelect from 'react-multiple-select-dropdown-lite'
import 'react-multiple-select-dropdown-lite/dist/index.css'
const options = [
{ label: 'Anywhere', value: 'any' },
{
type: 'group',
title: 'Europe',
childs: [
{ label: 'Berlin', value: 'berlin' },
{ label: 'Lisbon', value: 'lisbon' },
{
type: 'group',
title: 'Nordics',
childs: [
{ label: 'Oslo', value: 'oslo' },
{ label: 'Stockholm', value: 'stockholm' }
]
}
]
},
{
type: 'group',
title: 'Asia',
collapsible: true,
collapsed: true,
childs: [
{ label: 'Dhaka', value: 'dhaka' },
{ label: 'Tokyo', value: 'tokyo' }
]
},
{
type: 'group',
title: 'Antarctica',
emptyDataLabel: 'No offices yet',
childs: []
}
]
export default function Groups() {
return <MultiSelect options={options} placeholder='Offices' />
}

With type: 'submenu', the title is a row that opens its childs in a popover beside the menu: on a pause over it, a click, Enter or Arrow Right. With the highlight in it, Arrow Left or Escape close it again. Submenus nest to any depth and can hold groups. Search lists their options flat.

Submenus.tsx
import MultiSelect from 'react-multiple-select-dropdown-lite'
import 'react-multiple-select-dropdown-lite/dist/index.css'
const options = [
{ label: 'Anywhere', value: 'any' },
{
type: 'submenu',
title: 'Europe',
childs: [
{ label: 'Berlin', value: 'berlin' },
{ label: 'Lisbon', value: 'lisbon' },
{
type: 'submenu',
title: 'Nordics',
childs: [
{ label: 'Oslo', value: 'oslo' },
{ label: 'Stockholm', value: 'stockholm' }
]
}
]
},
{
type: 'submenu',
title: 'Asia',
childs: [
{ label: 'Dhaka', value: 'dhaka' },
{ label: 'Tokyo', value: 'tokyo' }
]
}
]
export default function Submenus() {
return <MultiSelect options={options} placeholder='Offices' />
}

The two mix at any depth: collapsible groups inside a submenu, and a submenu inside a collapsible group. Collapsing a group hides its submenus’ titles too.

NestedMenus.tsx
import MultiSelect from 'react-multiple-select-dropdown-lite'
import 'react-multiple-select-dropdown-lite/dist/index.css'
// collapsible groups inside submenus, and submenus inside collapsible groups
const options = [
{ label: 'All products', value: 'all' },
{
type: 'submenu',
title: 'Electronics',
childs: [
{
type: 'group',
title: 'Phones',
collapsible: true,
childs: [
{ label: 'iPhone', value: 'iphone' },
{ label: 'Pixel', value: 'pixel' }
]
},
{
type: 'group',
title: 'Laptops',
collapsible: true,
collapsed: true,
childs: [
{ label: 'MacBook', value: 'macbook' },
{ label: 'ThinkPad', value: 'thinkpad' }
]
},
{
type: 'submenu',
title: 'Accessories',
childs: [
{ label: 'Chargers', value: 'chargers' },
{ label: 'Cases', value: 'cases' }
]
}
]
},
{
type: 'group',
title: 'Clothing',
collapsible: true,
childs: [
{ label: 'Shirts', value: 'shirts' },
{ label: 'Shoes', value: 'shoes' },
{
type: 'submenu',
title: 'Kids',
childs: [
{ label: 'Jackets', value: 'jackets' },
{ label: 'Sneakers', value: 'sneakers' }
]
}
]
}
]
export default function NestedMenus() {
return <MultiSelect options={options} placeholder='Categories' />
}

renderGroupTitle renders the title rows of groups and submenus from { group, title, total, selected, open, icon }: total counts the options in it, nested ones too, and selected those picked. icon is the expand icon, for you to place or leave out.

Apple
Carrot
GroupTitles.tsx
import MultiSelect from 'react-multiple-select-dropdown-lite'
import 'react-multiple-select-dropdown-lite/dist/index.css'
const options = [
{
type: 'group',
title: 'Fruits',
collapsible: true,
childs: [
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' },
{ label: 'Cherry', value: 'cherry' }
]
},
{
type: 'submenu',
title: 'Vegetables',
childs: [
{ label: 'Carrot', value: 'carrot' },
{ label: 'Pepper', value: 'pepper' }
]
}
]
export default function GroupTitles() {
return (
<MultiSelect
options={options}
defaultValue={['apple', 'carrot']}
// the icon last, where the stylesheet holds it at the end of the row
renderGroupTitle={({ title, total, selected, icon }) => (
<>
{title}
<span>
{selected}/{total}
</span>
{icon}
</>
)}
/>
)
}

For an icon of your own, leave icon out and pick one from open. Push it to the end yourself: the stylesheet’s auto margin is on .msl-expand-icn only. A submenu is open while its popover shows, so check group.type to keep its icon pointing right:

renderGroupTitle={({ title, total, selected, open, group }) => (
<>
{title}
<span>{selected}/{total}</span>
{open && group.type !== 'submenu' ? (
<ChevronDown aria-hidden className='ms-auto' />
) : (
<ChevronRight aria-hidden className='ms-auto' />
)}
</>
)}

The stylesheet doesn’t turn or mirror your icon, so it is up to you right to left.

All in CSS: the open box grows to fit the options and shrinks back as they fade, submenus slide in and fade out, a collapsible group slides open and shut, and the chevrons turn. Whatever closes has data-closing until its exit transitions end. See Animations for how.

Animated.tsx
import MultiSelect from 'react-multiple-select-dropdown-lite'
import 'react-multiple-select-dropdown-lite/dist/index.css'
import './Animated.css'
const options = [
{ label: 'All products', value: 'all' },
{
type: 'submenu',
title: 'Electronics',
childs: [
{ label: 'Phones', value: 'phones' },
{ label: 'Laptops', value: 'laptops' },
{
type: 'submenu',
title: 'Accessories',
childs: [
{ label: 'Chargers', value: 'chargers' },
{ label: 'Cases', value: 'cases' }
]
}
]
},
{
type: 'group',
title: 'Clothing',
collapsible: true,
childs: [
{ label: 'Shirts', value: 'shirts' },
{ label: 'Shoes', value: 'shoes' }
]
}
]
export default function Animated() {
return (
<MultiSelect
className='animated'
options={options}
placeholder='Animated'
/>
)
}
Animated.css
@media (prefers-reduced-motion: no-preference) {
/* the open box grows to fit the options, and shrinks back: their grid row goes from 0fr to 1fr */
.animated .msl {
grid-template-rows: max-content 0fr;
transition:
grid-template-rows 200ms,
box-shadow 200ms;
}
.animated .msl-active {
grid-template-rows: max-content 1fr;
}
.animated .msl-active[data-closing] {
grid-template-rows: max-content 0fr;
box-shadow: none;
}
/* opening upward, the options come first: they only fade */
.animated .msl-active-up {
grid-template-rows: 1fr max-content;
}
.animated .msl-options {
transition: opacity 200ms;
@starting-style {
opacity: 0;
}
}
.animated .msl[data-closing] .msl-options {
opacity: 0;
}
/* a submenu fades in, and its copy out */
.animated .msl-submenu {
transition:
opacity 150ms,
translate 150ms;
@starting-style {
opacity: 0;
translate: -6px 0;
}
&[data-closing] {
opacity: 0;
}
}
/* a collapsible group slides open and shut: the one box in its body, a grid row from 0fr to 1fr.
Its options fade too */
.animated .msl-grp-body {
display: grid;
grid-template-rows: 1fr;
transition: grid-template-rows 200ms;
@starting-style {
grid-template-rows: 0fr;
}
&[data-closing] {
grid-template-rows: 0fr;
}
> div {
overflow: hidden;
min-height: 0;
}
}
.animated .msl-grp-body .msl-option {
transition: opacity 200ms;
@starting-style {
opacity: 0;
}
}
.animated .msl-grp-body[data-closing] .msl-option {
opacity: 0;
}
/* the chevrons turn */
.animated .msl-expand-icn {
transition: rotate 150ms;
}
}

The same with Tailwind utilities and no stylesheet: see Tailwind animations.

With customValue, typing text that matches no option offers Create “…”. Enter or comma adds it; created values are listed under Custom Values.

react

value: ["react"]

Creatable.tsx
import { useState } from 'react'
import MultiSelect from 'react-multiple-select-dropdown-lite'
import 'react-multiple-select-dropdown-lite/dist/index.css'
const options = [
{ label: 'react', value: 'react' },
{ label: 'typescript', value: 'typescript' },
{ label: 'css', value: 'css' }
]
export default function Creatable() {
const [value, setValue] = useState(['react'])
return (
<>
<MultiSelect
customValue
options={options}
defaultValue={['react']}
onChange={setValue}
placeholder='Type a tag, then Enter or comma'
/>
<p className='demo-output'>value: {JSON.stringify(value)}</p>
</>
)
}

Options can be any shape: getLabel and getValue read them. onChange gets the selected objects back as its second argument. Values created by customValue are always { label, value }. The fields disabled, style, classes and type keep their meaning in any shape.

[]

OptionObjects.tsx
import { useState } from 'react'
import MultiSelect from 'react-multiple-select-dropdown-lite'
import 'react-multiple-select-dropdown-lite/dist/index.css'
// straight from an API: no mapping to { label, value }
const users = [
{ id: 'u1', name: 'Ada Lovelace', email: 'ada@example.com' },
{ id: 'u2', name: 'Grace Hopper', email: 'grace@example.com' },
{ id: 'u3', name: 'Alan Turing', email: 'alan@example.com' }
]
export default function OptionObjects() {
const [people, setPeople] = useState<typeof users>([])
return (
<>
<MultiSelect
options={users}
getLabel={(user) => user.name}
getValue={(user) => user.id}
onChange={(_ids, selected) => setPeople(selected)}
placeholder='Invite people'
/>
<p className='demo-output'>
{JSON.stringify(people.map(({ id, email }) => ({ id, email })))}
</p>
</>
)
}

Pass value and update it from onChange to own the selection, for example to reset it or set it from elsewhere. defaultValue only sets the initial selection.

Read

value: ["read"]

Controlled.tsx
import { useState } from 'react'
import MultiSelect from 'react-multiple-select-dropdown-lite'
import 'react-multiple-select-dropdown-lite/dist/index.css'
const options = [
{ label: 'Read', value: 'read' },
{ label: 'Write', value: 'write' },
{ label: 'Delete', value: 'delete' },
{ label: 'Admin', value: 'admin' }
]
export default function Controlled() {
const [value, setValue] = useState(['read'])
return (
<>
<div className='demo-row'>
<MultiSelect options={options} value={value} onChange={setValue} />
<button onClick={() => setValue(options.map((o) => o.value))}>
Select all
</button>
<button onClick={() => setValue(['read'])}>Reset</button>
</div>
<p className='demo-output'>value: {JSON.stringify(value)}</p>
</>
)
}

limit caps how many values can be picked.

Limit.tsx
import MultiSelect from 'react-multiple-select-dropdown-lite'
import 'react-multiple-select-dropdown-lite/dist/index.css'
const options = ['Red', 'Green', 'Blue', 'Yellow', 'Purple'].map((c) => ({
label: c,
value: c.toLowerCase()
}))
// the wrapper gets the msl-limit-reached class once the limit is hit
export default function Limit() {
return <MultiSelect options={options} limit={2} placeholder='Pick up to 2' />
}

searchInMenu puts the search field at the top of the open menu. The closed control shows only the values, or placeholder when there are none, with no search placeholder next to the chips. searchPlaceholder labels the field. Single select and disableChip can then search with a value selected.

SearchInMenu.tsx
import MultiSelect from 'react-multiple-select-dropdown-lite'
import 'react-multiple-select-dropdown-lite/dist/index.css'
const options = [
{ label: 'JavaScript', value: 'js' },
{ label: 'TypeScript', value: 'ts' },
{ label: 'Python', value: 'py' },
{ label: 'Rust', value: 'rs' },
{ label: 'Go', value: 'go' }
]
// the closed control shows only the values; the search field opens with the menu
export default function SearchInMenu() {
return (
<div className='demo-row'>
<MultiSelect
searchInMenu
options={options}
defaultValue={['js', 'ts']}
searchPlaceholder='Search languages'
/>
<MultiSelect
searchInMenu
singleSelect
options={options}
defaultValue={['rs']}
/>
</div>
)
}

Backspace in an empty search field removes the last chip. deleteOnBackspace={false} turns that off, so only a chip’s delete button removes it.

Design
Frontend
DeleteOnBackspace.tsx
import MultiSelect from 'react-multiple-select-dropdown-lite'
import 'react-multiple-select-dropdown-lite/dist/index.css'
const options = ['Design', 'Frontend', 'Backend', 'DevOps'].map((t) => ({
label: t,
value: t.toLowerCase()
}))
// Backspace only edits the search text: chips go through their × button
export default function DeleteOnBackspace() {
return (
<MultiSelect
deleteOnBackspace={false}
options={options}
defaultValue={['design', 'frontend']}
/>
)
}

disableChip shows the single label, or a count with chipAlternateText.

DisableChip.tsx
import MultiSelect from 'react-multiple-select-dropdown-lite'
import 'react-multiple-select-dropdown-lite/dist/index.css'
const options = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map((d) => ({
label: d,
value: d.toLowerCase()
}))
// one label, or a count once more than one is picked
export default function DisableChip() {
return (
<MultiSelect
disableChip
chipAlternateText='days selected'
options={options}
defaultValue={['mon', 'wed', 'fri']}
/>
)
}

getLabel(option) renders each option, chip and single value. Return any JSX. Search matches what getLabel returns when it is text, else the option’s label.

JsxLabels.tsx
import MultiSelect from 'react-multiple-select-dropdown-lite'
import 'react-multiple-select-dropdown-lite/dist/index.css'
const Status = ({ color, text }: { color: string; text: string }) => (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
<span
style={{ width: 10, height: 10, borderRadius: '50%', background: color }}
/>
{text}
</span>
)
// `label` stays text for search; getLabel decides what shows
const options = [
{ label: 'Online', value: 'online', color: '#22c55e' },
{ label: 'Away', value: 'away', color: '#f59e0b' },
{ label: 'Busy', value: 'busy', color: '#ef4444' }
]
export default function JsxLabels() {
return (
<MultiSelect
options={options}
getLabel={(opt) => <Status color={opt.color} text={opt.label} />}
placeholder='Status'
/>
)
}

downArrowIcon, closeIcon, and the icons after a collapsible group’s title (groupIcon) and a submenu’s (submenuIcon). To move or resize them with CSS, see Expand icons.

North
CustomIcons.tsx
import MultiSelect from 'react-multiple-select-dropdown-lite'
import 'react-multiple-select-dropdown-lite/dist/index.css'
const options = [
{ label: 'North', value: 'n' },
{ label: 'South', value: 's' },
{
type: 'group',
title: 'Diagonals',
collapsible: true,
childs: [
{ label: 'North East', value: 'ne' },
{ label: 'South West', value: 'sw' }
]
},
{
type: 'submenu',
title: 'More',
childs: [
{ label: 'East', value: 'e' },
{ label: 'West', value: 'w' }
]
}
]
export default function CustomIcons() {
return (
<MultiSelect
options={options}
defaultValue={['n']}
downArrowIcon={<span aria-hidden>🔽</span>}
closeIcon={<span aria-hidden>✕</span>}
// drawn pointing right: the stylesheet turns the group's down while it is open
groupIcon='▸'
submenuIcon='→'
/>
)
}
Alpha
Beta
Disabled.tsx
import MultiSelect from 'react-multiple-select-dropdown-lite'
import 'react-multiple-select-dropdown-lite/dist/index.css'
const options = [
{ label: 'Alpha', value: 'a' },
{ label: 'Beta', value: 'b' }
]
export default function Disabled() {
return <MultiSelect disabled options={options} defaultValue={['a', 'b']} />
}

Options render only while the menu is open, so long lists don’t slow the page down. For thousands of options, render only the rows in view through a virtual list.

LargeData.tsx
import MultiSelect from 'react-multiple-select-dropdown-lite'
import 'react-multiple-select-dropdown-lite/dist/index.css'
const options = Array.from({ length: 5000 }, (_, i) => ({
label: `Item ${i + 1}`,
value: `item-${i + 1}`
}))
// options render only while the menu is open
export default function LargeData() {
return <MultiSelect options={options} placeholder='5000 items' />
}

renderList renders the rows through a virtual list library of your choice, here TanStack Virtual, so only the rows in view render. Every kind of row works: groups in groups, collapsible and collapsed groups, submenus in groups and groups in submenus, an empty group, and 11,000 options in a collapsed group and a submenu. See virtual lists for react-window and other libraries.

TanstackVirtual.tsx
import { useVirtualizer } from '@tanstack/react-virtual'
import { useEffect, useRef } from 'react'
import MultiSelect from 'react-multiple-select-dropdown-lite'
import type { MultiSelectListProps } from 'react-multiple-select-dropdown-lite'
import 'react-multiple-select-dropdown-lite/dist/index.css'
// renders only the rows in view, measured: group titles are shorter than options
export function TanstackList({
count,
row,
activeIndex
}: MultiSelectListProps) {
const ref = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count,
getScrollElement: () => ref.current,
estimateSize: () => 36
})
// keeps the keyboard highlight in view
useEffect(() => {
activeIndex >= 0 && virtualizer.scrollToIndex(activeIndex)
}, [activeIndex])
return (
// no tab stop: Firefox makes a scroller one
<div ref={ref} tabIndex={-1} style={{ overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
{virtualizer.getVirtualItems().map((item) => (
<div
key={item.key}
data-index={item.index}
ref={virtualizer.measureElement}
style={{
position: 'absolute',
width: '100%',
// top, not a transform: Firefox places a submenu off a transformed row
top: item.start
}}
>
{row(item.index)}
</div>
))}
</div>
</div>
)
}
const items = (name: string, count: number) =>
Array.from({ length: count }, (_, i) => ({
label: `${name} ${i + 1}`,
value: `${name.toLowerCase()}-${i + 1}`
}))
// every kind of row: a disabled option, a group in a group, collapsible and collapsed groups, a submenu in a
// group, a group in a submenu, a submenu in a submenu, an empty group and long lists
export const options = [
{ label: 'Apple', value: 'apple' },
{ label: 'Dragon fruit (out of season)', value: 'dragon', disabled: true },
{
type: 'group',
title: 'Berries',
childs: [
{ label: 'Blueberry', value: 'blueberry' },
{
type: 'group',
title: 'Wild',
childs: [{ label: 'Cloudberry', value: 'cloudberry' }]
}
]
},
{
type: 'group',
title: 'Citrus',
collapsible: true,
childs: [
{ label: 'Lemon', value: 'lemon' },
{
type: 'submenu',
title: 'More citrus',
childs: [
{ label: 'Grapefruit', value: 'grapefruit' },
{ label: 'Yuzu', value: 'yuzu' }
]
}
]
},
{
type: 'submenu',
title: 'Vegetables',
childs: [
{ label: 'Carrot', value: 'carrot' },
{
type: 'group',
title: 'Leafy greens',
collapsible: true,
collapsed: true,
childs: [
{ label: 'Kale', value: 'kale' },
{ label: 'Spinach', value: 'spinach' }
]
},
{
type: 'submenu',
title: '1,000 more',
childs: items('Vegetable', 1000)
}
]
},
{
type: 'group',
title: 'Exotic',
childs: [],
emptyDataLabel: 'Out of season'
},
{
type: 'group',
title: '10,000 items',
collapsible: true,
collapsed: true,
childs: items('Item', 10_000)
}
]
export default function TanstackVirtual() {
return (
<MultiSelect
options={options}
placeholder='Every kind of row'
renderList={(list) => <TanstackList {...list} />}
/>
)
}