Skip to content

Styling

Import the stylesheet once:

import 'react-multiple-select-dropdown-lite/dist/index.css'

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;
}

Colors are variables too, so a theme is one rule.

Ocean
Theming.tsx
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'
/>
)
}
Theming.css
/* .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 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 }).

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.

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.

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. onMenuClose fires 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 0fr to 1fr, as below. The open box: .msl is a grid, the options its second row. Give the control’s row max-content: an auto row would take the space a part fr row leaves. A collapsible group: .msl-grp-body holds one <div> for this. Make the body a grid with one 1fr row, 0fr in @starting-style and with data-closing, and give the <div> overflow: hidden; min-height: 0.
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;
}
}