Examples
Every demo below runs the code under it.
Multi select
Section titled “Multi select”value: []
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> </> )}Single select
Section titled “Single select”singleSelect keeps one value and closes the menu after a pick. Disabled options stay visible but can’t be picked.
value: ["m"]
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
Section titled “Groups”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.
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' />}Submenus
Section titled “Submenus”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.
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' />}Collapsible groups and submenus together
Section titled “Collapsible groups and submenus together”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.
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 groupsconst 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' />}Group titles
Section titled “Group titles”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.
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.
Animations
Section titled “Animations”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.
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' /> )}@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.
Creatable values
Section titled “Creatable values”With customValue, typing text that matches no option offers Create “…”. Enter or comma adds it; created values are listed under Custom Values.
value: ["react"]
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> </> )}Option objects
Section titled “Option objects”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.
[]
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> </> )}Controlled value
Section titled “Controlled value”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.
value: ["read"]
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.
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 hitexport default function Limit() { return <MultiSelect options={options} limit={2} placeholder='Pick up to 2' />}Search in the menu
Section titled “Search in the menu”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.
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 menuexport 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> )}Delete chips with their button only
Section titled “Delete chips with their button only”Backspace in an empty search field removes the last chip. deleteOnBackspace={false} turns that off, so only a chip’s delete button removes it.
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 × buttonexport default function DeleteOnBackspace() { return ( <MultiSelect deleteOnBackspace={false} options={options} defaultValue={['design', 'frontend']} /> )}Count instead of chips
Section titled “Count instead of chips”disableChip shows the single label, or a count with chipAlternateText.
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 pickedexport default function DisableChip() { return ( <MultiSelect disableChip chipAlternateText='days selected' options={options} defaultValue={['mon', 'wed', 'fri']} /> )}Custom labels
Section titled “Custom labels”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.
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 showsconst 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' /> )}Custom icons
Section titled “Custom icons”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.
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='→' /> )}Disabled
Section titled “Disabled”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']} />}Large lists
Section titled “Large lists”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.
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 openexport default function LargeData() { return <MultiSelect options={options} placeholder='5000 items' />}Virtual list
Section titled “Virtual list”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.
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 optionsexport 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 listsexport 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} />} /> )}