Styling
Import the stylesheet once:
import 'react-multiple-select-dropdown-lite/dist/index.css'CSS variables
Section titled “CSS variables”The defaults live on .msl-vars, the wrapper of every dropdown. Override them on .msl-vars for all dropdowns, or add your own class for one. Keep .msl-vars in the selector: a lone class of yours ties with the defaults, so it only wins when its CSS loads later.
| Variable | Default | Controls |
|---|---|---|
--msl-input-width |
300px |
width |
--msl-menu-max-height |
400px |
menu height before it scrolls |
--msl-font-size |
14px |
font size |
--msl-line-height |
1.4 |
unitless line height |
--msl-border-radius |
8px |
corner radius |
--msl-active-menu-background |
white |
background while open |
--msl-active-menu-shadow |
5 soft layers | shadow while open |
--msl-active-menu-radius |
var(--msl-border-radius) |
corner radius while open |
--msl-active-menu-border-color |
hsl(224deg 5.68% 88.24%) |
border while open |
--msl-divider-color |
rgb(3 7 18 / 0.08) |
line between the input and the open menu |
--msl-placeholder-color |
rgb(146 146 146) |
placeholder text color |
--msl-border-color |
rgb(156 156 156) |
border |
--msl-hover-border-color |
rgb(80 80 80) |
border on hover |
--msl-disabled-background |
rgb(246 246 246) |
background when disabled |
--msl-chip-background |
rgb(238 238 238) |
chip |
--msl-chip-delete-background |
transparent |
chip delete button |
--msl-chip-delete-color |
rgb(0 0 0 / 0.6) |
chip delete icon |
--msl-button-hover-background |
lightgray |
clear, arrow and chip delete buttons on hover |
--msl-option-hover-background |
rgb(233 233 233) |
option on hover or keyboard highlight |
--msl-option-active-background |
hsl(209.49deg 10.01% 93.31%) |
selected option |
--msl-option-active-color |
#0351ff |
selected option text |
--msl-option-disabled-color |
rgb(0 0 0 / 0.3) |
disabled option text |
--msl-group-title-background |
#fafafa |
group title |
--msl-group-offset |
12px |
indent of a group’s options, per level |
--msl-focus-color |
rgb(0 119 255) |
keyboard focus outline |
--msl-background |
transparent |
background while closed |
--msl-scrollbar-color |
rgb(0 0 0 / 0.1) |
menu scrollbar thumb |
/* every dropdown */.msl-vars { --msl-input-width: 100%;}/* one dropdown: <MultiSelect className='compact' /> */.msl-vars.compact { --msl-font-size: 12px;}Dark theme
Section titled “Dark theme”Colors are variables too, so a theme is one rule.
import MultiSelect from 'react-multiple-select-dropdown-lite'import 'react-multiple-select-dropdown-lite/dist/index.css'import './Theming.css'
const options = [ { label: 'Midnight', value: 'midnight' }, { label: 'Ocean', value: 'ocean' }, { label: 'Forest', value: 'forest' }, { label: 'Sunset', value: 'sunset' }]
export default function Theming() { return ( <MultiSelect className='dark-msl' options={options} defaultValue={['ocean']} placeholder='Theme' /> )}/* .msl-vars as well, so the variables win over the defaults whatever the CSS order */.msl-vars.dark-msl { --msl-input-width: 320px; --msl-border-radius: 6px; --msl-font-size: 15px; --msl-background: #1f2430; --msl-active-menu-background: #1f2430; --msl-active-menu-shadow: 0 6px 20px rgba(0, 0, 0, 0.55); --msl-active-menu-border-color: #3a4152; --msl-divider-color: rgb(255 255 255 / 0.08); --msl-border-color: #3a4152; --msl-hover-border-color: #5b6580; --msl-chip-background: #343b4d; --msl-chip-delete-background: #4a5268; --msl-chip-delete-color: #e6e6e6; --msl-button-hover-background: #3a4152; --msl-option-hover-background: #2c3242; --msl-option-active-background: #22395c; --msl-option-active-color: #8fb8ff; --msl-group-title-background: #2a3040; color: #e6e6e6;}The open menu
Section titled “The open menu”The open menu is one box: the input box, .msl.msl-active (.msl-active-up opening upward), with the options attached. It is a popover over the dropdown, in the top layer, so overflow: hidden parents, stacking contexts and z-indexes don’t clip or cover it, in a modal <dialog> too. It stays in place in the DOM, so your CSS reaches it through its ancestors and it inherits the text color, font and direction there. The dropdown keeps the input box’s height. The menu follows the dropdown as the page scrolls, and lines up, scaled as the dropdown is, in a transformed dialog or a scaled part of the page. It is as wide as the dropdown and clips the options: to widen the menu, widen the open box (.msl-active { min-width: 400px }).
Class names
Section titled “Class names”State classes:
| Class | On | When |
|---|---|---|
msl-disabled |
wrapper | disabled |
msl-limit-reached |
wrapper | limit is reached |
msl-active / msl-active-up |
.msl |
menu is open, below / above the input |
msl-option-active |
option | selected |
msl-option-focused |
option | highlighted by the keyboard |
msl-option-disable |
option | disabled |
Parts: msl-wrp (wrapper), msl (input box, and the open menu), msl-chip, msl-chip-delete, msl-single-value, msl-input, msl-search (the search field’s box with searchInMenu), msl-clear-btn, msl-arrow-btn, msl-options (menu), msl-option, msl-grp (a group), msl-grp-title, msl-grp-body (a collapsible group’s childs), msl-submenu (a submenu’s popover), msl-expand-icn (the icon after a title that opens).
Where the browser has CSS anchor positioning, a submenu shows in the top layer. Elsewhere it opens under its title, in the menu, with the same look. Right to left (a dir="rtl" attribute, which the stylesheet reads with :dir(), not CSS direction alone), it opens on the left, and Arrow Left and Right swap. The stylesheet places it with CSS anchor positioning: beside its title, on the other side or above where there is no room, and over its title where neither side has room. Its title is its position-anchor, so your own .msl-submenu rules can place it elsewhere with another position-area, or with anchor() insets and position-area: none. Submenu titles and collapsible group titles have aria-expanded, and end with an icon: see Expand icons.
A single option can carry its own classes and style. To add classes to every part, Tailwind utilities for example, use the classNames prop: see Tailwind.
The component’s inline styles are behavior, not looks: display: contents on the chip wrapper and on the box around the options (around each row with renderList), which keeps those boxes out of the layout with or without the stylesheet (select options with .msl-options .msl-option, not >), and, while the menu is open, the dropdown’s min-height (the open box left the flow) and the popover’s position, coordinates, width and scale, and a reset of the browser’s popover look (inset, margin, padding, border, background, color: inherit, overflow), on the unstyled <div> around .msl, and, on an open submenu and its title, the position-anchor and anchor-name that tie them together, and, on a closed submenu’s copy, the position it had. Everything else is in the stylesheet: without it the markup is unstyled, and plain CSS overrides any of its rules.
Expand icons
Section titled “Expand icons”Collapsible group titles and submenu titles end with a <span class="msl-expand-icn" aria-hidden="true">. By default it holds the arrow button’s chevron SVG, .msl-chevron-icn, which the stylesheet sizes, thickens (stroke-width) and turns to point to the end. Pass your own icon with groupIcon and submenuIcon, drawn pointing to the inline end (right): the stylesheet turns the span down while its group is open (rotate: 90deg) and to the left right to left (180deg). It goes in a <bdo dir="ltr">, so the browser doesn’t mirror a text arrow such as › as well.
<MultiSelect options={options} groupIcon={<ChevronRight size={14} />} submenuIcon={<ArrowRight size={14} />}/>The title is a flex row, and the icon’s margin-inline-start: auto holds it at the end. Every rule is one class, so yours override them:
/* before the title */.msl-grp-title > .msl-expand-icn { order: -1; margin-inline-start: 0;}
/* a bigger, thinner default chevron */.msl-chevron-icn { width: 20px; height: 20px; stroke-width: 1.5;}
/* an icon that shows the state itself (+ / -): no turn */.msl-grp-title > .msl-expand-icn { rotate: none;}With Tailwind, the same through classNames.groupIcon and classNames.submenuIcon: '-order-1 ms-0'.
renderGroupTitle places the icon in JS: it gets the span as icon, and open to draw a state of your own instead. Put first, the span still has the auto margin: give it margin-inline-start: 0 as above.
Animations
Section titled “Animations”Animate opening and closing with CSS, or with Tailwind’s variants. The component sets no durations.
Opening needs nothing from the component. The options, a submenu and the rows of an expanding group are new elements, so @starting-style (Tailwind’s starting:) or a keyframe animation brings them in. State changes transition as any CSS does: aria-expanded, aria-selected, data-focused, data-open.
Closing is where the component helps. Whatever closes stays until the CSS animations and transitions that its data-closing starts have ended:
| Closing | Marked |
|---|---|
| the menu | .msl[data-closing], the open box, still .msl-active and data-open |
| a submenu | a copy of its .msl-submenu, with data-closing, where the submenu was |
| a collapsible group’s childs | a copy of its .msl-grp-body, with data-closing, where it was |
- Without exit CSS, things close at once, as they always have. Transitions already running when it closes, such as an option’s hover background, don’t hold it, and neither does an infinite animation.
- A closing menu and the copies are inert: no clicks, out of the accessibility tree. Focus leaves the menu as it starts to close.
onMenuClosefires then too, and the combobox reports itself collapsed. - Opening the menu again before it has gone brings the same menu back, mid animation. A submenu or group that opens again replaces its copy.
- A keyframe exit animation needs
forwards, so its end state holds until the element goes. - Heights animate through a grid row from
0frto1fr, as below. The open box:.mslis a grid, the options its second row. Give the control’s rowmax-content: anautorow would take the space a partfrrow leaves. A collapsible group:.msl-grp-bodyholds one<div>for this. Make the body a grid with one1frrow,0frin@starting-styleand withdata-closing, and give the<div>overflow: hidden; min-height: 0.
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; }}