Tailwind
Skip dist/index.css and pass Tailwind classes for each part through classNames. Each string goes after the part’s msl-* class. With no stylesheet the markup is unstyled, and the select still works: opening, the menu above or below, disabled.
value: []
import { useState } from 'react'import MultiSelect from 'react-multiple-select-dropdown-lite'// no dist/index.css: Tailwind styles every partimport './tailwind-demos.css'
const options = [ { label: 'Apple', value: 'apple' }, { label: 'Banana', value: 'banana' }, { label: 'Cherry', value: 'cherry' }, { label: 'Mango', value: 'mango' }, { label: 'Orange', value: 'orange', disabled: true }, { type: 'group', title: 'Berries', childs: [ { label: 'Blueberry', value: 'blueberry' }, { label: 'Strawberry', value: 'strawberry' } ] }]
const button = 'inline-flex cursor-pointer items-center justify-center rounded-full'
const classNames = { // open, it holds the options too, in the top layer over the page: it sets its own background control: 'grid cursor-text grid-cols-[minmax(0,1fr)_auto] overflow-hidden rounded-lg border border-gray-400 not-data-open:hover:border-gray-600 data-open:border-gray-200 data-open:bg-white data-open:shadow-lg in-data-disabled:bg-gray-50', values: 'flex min-w-0 flex-wrap items-center gap-1 p-1', // with searchInMenu, the search field's box heading the options search: 'col-span-full flex border-t border-gray-200 p-1 in-data-up:border-t-0 in-data-up:border-b', input: 'min-w-0 flex-1 border-0 bg-transparent p-1 [font:inherit] text-inherit outline-none placeholder:text-gray-400 empty:before:text-gray-400 empty:before:content-[attr(data-placeholder)]', singleValue: 'truncate p-1', chip: 'inline-flex items-center gap-1 rounded-md bg-gray-100 py-0.5 pr-0.5 pl-1.5', chipDelete: `${button} size-5 text-black/60 hover:bg-gray-300 [&_svg]:size-2.5`, actions: 'flex items-center pr-1', clearButton: `${button} size-6 hover:bg-gray-200 [&_svg]:size-2.5`, arrowButton: `${button} size-6 hover:bg-gray-200 in-data-open:rotate-180 [&_svg]:size-5`, // --msl-menu-room is set on the menu; the divider sits on the side toward the control, below it or (data-up) above. // It fades in, and out: closing, the control has data-closing until the fade ends menu: 'col-span-full max-h-[min(400px,var(--msl-menu-room))] overflow-auto border-t border-gray-200 py-1 in-data-up:border-t-0 in-data-up:border-b transition-opacity duration-150 starting:opacity-0 in-data-closing:opacity-0', option: 'mx-1.5 my-1 cursor-pointer rounded-md px-2 py-1 hover:bg-gray-100 aria-selected:text-blue-600 aria-disabled:cursor-default aria-disabled:text-gray-300 data-focused:bg-gray-100', // a step in per level, the title at its group's level group: 'ps-3', groupTitle: 'my-1 me-1.5 -ms-1.5 rounded-md bg-gray-50 px-2 py-1 text-xs text-gray-500', empty: 'mx-1.5 my-1 px-2 py-1 text-gray-400'}
export default function Tailwind() { const [value, setValue] = useState<string[]>([])
return ( <> <MultiSelect options={options} onChange={setValue} className='w-75 rounded-lg text-sm' classNames={classNames} /> <p className='demo-output'>value: {JSON.stringify(value)}</p> </> )}| Key | Element |
|---|---|
className |
the wrapper only (a prop of its own, not in classNames) |
control |
msl: the box with the values and buttons; open, the options too, in the top layer |
values |
msl-input-wrp: chips, single value and search field |
input |
msl-input: the search field, or the placeholder when there is none |
search |
msl-search: the box around the search field with searchInMenu, atop the options |
singleValue |
msl-single-value: the value with singleSelect, the count with disableChip |
chip |
msl-chip (default chips; a customChip gets classNames in its props) |
chipDelete |
msl-chip-delete |
actions |
msl-actions: the clear and arrow buttons |
clearButton |
msl-clear-btn |
arrowButton |
msl-arrow-btn |
menu |
msl-options: the listbox |
group |
msl-grp: a group’s box |
groupTitle |
msl-grp-title; a collapsible group’s has aria-expanded |
groupIcon |
msl-expand-icn after a collapsible group’s title, around groupIcon |
groupBody |
msl-grp-body: a collapsible group’s childs, in one <div> (for height animations) |
option |
msl-option, and a submenu’s title, which has aria-expanded |
submenuIcon |
msl-expand-icn after a submenu’s title, around submenuIcon |
submenu |
msl-submenu: a submenu’s popover, placed beside its title by the stylesheet |
empty |
emptyDataLabel, and an empty group’s |
State is in attributes, so Tailwind’s variants reach it:
| Variant | Matches |
|---|---|
aria-selected: |
a selected option |
aria-disabled: |
a disabled option |
data-focused: |
the option or title highlighted by the keyboard |
aria-expanded: |
an open submenu’s title, an expanded group’s title |
data-open: |
the open control; in-data-open: for parts inside it |
data-up: |
the open control, opened upward |
data-closing: |
what is closing: the control, a submenu’s copy |
in-data-closing: |
parts inside it, the rows of a collapsing group too |
in-data-disabled: |
parts of a disabled select (the wrapper has data-disabled) |
The open menu
Section titled “The open menu”- Open, the control holds the options and shows in the top layer, over the wrapper. Give it a background (
data-open:bg-white): nothing is behind it. A grid with the options across both columns (col-span-full) puts them under the values and buttons. - It stays in place in the DOM, so it inherits the text color and font around the select, and needs no z-index.
- Without
renderList, the options sit in adisplay: contentsbox in the menu, made anew for each search: style them throughoption, not with*:orspace-y-*onmenu. - Opening upward, the options come before the values in the DOM, and the control gets
data-up. A divider on the options’border-t in-data-up:border-t-0 in-data-up:border-blands next to the control either way. WithsearchInMenu, the search field’s box (search) heads the options, on the side away from the control when opening upward, and takes the same divider. --msl-menu-roomon the options (classNames.menu) is the room the viewport leaves on the menu’s side. Cap the options with it:max-h-[min(400px,var(--msl-menu-room))]. Register it as non-inherited, as the example’s CSS does, so a scroll restyles the listbox, not every option. Its initial value caps the menu before it first measures the room, for a virtual list that measures itself on mount:
@property --msl-menu-room { syntax: '*'; inherits: false; initial-value: 100vh;}- With
renderList, the options and submenus getdata-custom. Make them flex columns,data-custom:flex data-custom:flex-col, so a virtual list’s scroller shrinks to the menu’s height, and give submenus a width,data-custom:w-75: their rows have none of their own.
Animations
Section titled “Animations”starting: animates parts in, and data-closing: / in-data-closing: out: whatever closes stays until its transitions end. The menu in the example above fades in and out with transition-opacity duration-150 starting:opacity-0 in-data-closing:opacity-0.
Below, the open box also grows to fit the options and shrinks back (a grid row from 0fr to 1fr), submenus slide in and fade out, a collapsible group slides open and shut (its body’s one <div> as a grid row), and the chevrons turn. With no stylesheet, the submenu’s classes place it too, with CSS anchor positioning: its title is its position-anchor.
import MultiSelect from 'react-multiple-select-dropdown-lite'// no dist/index.css: Tailwind styles and animates every partimport './tailwind-demos.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' } ] }]
const button = 'inline-flex cursor-pointer items-center justify-center rounded-full'// a title with aria-expanded, open or not: a row, its icon at the endconst opens = '[&[aria-expanded]]:flex [&[aria-expanded]]:items-center [&[aria-expanded]]:gap-2'// the icon after it: the arrow button's chevron, turned to point rightconst chevron = 'ms-auto flex shrink-0 transition-[rotate] *:size-[15px] *:-rotate-90 *:stroke-2'
const classNames = { // the open box grows to fit the options, and shrinks back: their grid row goes from 0fr to 1fr. // Opening upward the options come first: they only fade control: 'grid cursor-text grid-cols-[minmax(0,1fr)_auto] grid-rows-[max-content_0fr] overflow-hidden rounded-lg border border-gray-400 transition-[grid-template-rows] duration-200 not-data-open:hover:border-gray-600 data-open:grid-rows-[max-content_1fr] data-open:border-gray-200 data-open:bg-white data-open:shadow-lg data-open:data-up:grid-rows-[1fr_max-content] data-closing:not-data-up:grid-rows-[max-content_0fr]', values: 'flex min-w-0 flex-wrap items-center gap-1 p-1', input: 'min-w-0 flex-1 border-0 bg-transparent p-1 [font:inherit] text-inherit outline-none placeholder:text-gray-400 empty:before:text-gray-400 empty:before:content-[attr(data-placeholder)]', chip: 'inline-flex items-center gap-1 rounded-md bg-gray-100 py-0.5 pr-0.5 pl-1.5', chipDelete: `${button} size-5 text-black/60 hover:bg-gray-300 [&_svg]:size-2.5`, actions: 'flex items-center pr-1', clearButton: `${button} size-6 hover:bg-gray-200 [&_svg]:size-2.5`, arrowButton: `${button} size-6 transition-[rotate] hover:bg-gray-200 in-data-open:rotate-180 [&_svg]:size-5`, // no vertical padding: squeezed to 0fr, the row keeps the menu's padding in Chrome menu: 'col-span-full max-h-[min(400px,var(--msl-menu-room))] overflow-auto border-t border-gray-200 transition-opacity duration-200 in-data-up:border-t-0 in-data-up:border-b starting:opacity-0 in-data-closing:opacity-0', // a popover with no stylesheet: placed beside its title with CSS anchor positioning, on the other side without room submenu: 'm-0 -mt-1.5 inset-auto [position-area:right_span-bottom] max-h-[400px] w-max min-w-30 overflow-auto rounded-lg border border-gray-200 bg-white p-0 text-inherit shadow-lg transition-[opacity,translate] duration-150 [position-try-fallbacks:flip-inline,flip-block] starting:-translate-x-1.5 starting:opacity-0 data-closing:opacity-0 rtl:[position-area:none] rtl:top-[anchor(top)] rtl:right-[anchor(left)]', // in and out with the menu, a submenu, or their group option: `mx-1.5 my-1 cursor-pointer rounded-md px-2 py-1 transition-[opacity,background-color] duration-150 hover:bg-gray-100 aria-selected:text-blue-600 aria-disabled:cursor-default aria-disabled:text-gray-300 aria-expanded:bg-gray-100 data-focused:bg-gray-100 starting:opacity-0 in-data-closing:opacity-0 ${opens}`, submenuIcon: chevron, group: 'ps-3', // a collapsible group slides open and shut: the one box in its body, a grid row from 0fr to 1fr groupBody: 'grid grid-rows-[1fr] transition-[grid-template-rows] duration-200 starting:grid-rows-[0fr] data-closing:grid-rows-[0fr] *:min-h-0 *:overflow-hidden', groupTitle: `my-1 me-1.5 -ms-1.5 rounded-md bg-gray-50 px-2 py-1 text-xs text-gray-500 data-focused:bg-gray-100 [&[aria-expanded]]:cursor-pointer [&[aria-expanded]]:hover:bg-gray-100 group ${opens}`, // down while the group is open groupIcon: `${chevron} group-aria-expanded:rotate-90`, empty: 'mx-1.5 my-1 px-2 py-1 text-gray-400'}
export default function TailwindAnimated() { return ( <MultiSelect options={options} placeholder='Animated' className='w-75 rounded-lg text-sm' classNames={classNames} /> )}See Animations for what closes, and how.
With the default stylesheet
Section titled “With the default stylesheet”To restyle a few parts on top of the default look, import the stylesheet into a cascade layer below Tailwind’s utilities, or its rules beat them:
@import 'tailwindcss';@import 'react-multiple-select-dropdown-lite/dist/index.css' layer(components);