Modern React applications rarely need just a dropdown.
In real projects, a multiselect dropdown usually sits inside something more important: filters, dashboards, reporting tools, permission editors, workflow screens, admin panels, and data-heavy forms where users need to search, group, select, clear, validate, and keep state consistent. It is almost never the feature. It is the control that everything else in the form depends on.
That is the context behind Stackline React Multiselect Dropdown, maintained for React 19.
This is the React counterpart to the Stackline Angular line, and it is meant to behave like one. The two packages share the same combobox and ARIA contract, the same skin system, the same headless philosophy, the same per-major versioning, and the same live-example model. If your organization runs both Angular and React, the goal is that a multiselect feels and behaves the same way in both stacks, instead of being two unrelated controls that happen to share a name. In fact, the combobox contract was first defined and validated in this React line, then carried into the Angular line — so on the React side you are looking at the reference implementation.
React 19 is a good target for that. It is the version where React moved decisively toward explicit async state and less boilerplate: Actions and the new form hooks, the use hook for reading promises and context during render, ref as a plain prop instead of forwardRef, React Server Components as a production pattern, and the React Compiler taking over manual memoization. A maintained, accessibility-focused selection component should slot cleanly into that world without fighting it.
This release of the library is not just about making a dropdown render on the latest React. The goal is a practical component that can be adopted in real applications, exercised through live examples, and customized at whatever depth a team needs — from a drop-in styled component to a fully headless hook where the application owns every element and every line of CSS.
Install the React 19 line with:
npm install @stackline/react-multiselect-dropdown
Links:
npm:
https://www.npmjs.com/package/@stackline/react-multiselect-dropdown
Documentation (React 19):
https://alexandro.net/docs/react/multiselect/react-19/#/basic
Live demo / interactive playground (StackBlitz):
https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fbasic%2Fbasic.component.tsx&startScript=start&initialpath=%2Fbasic
GitHub:
https://github.com/alexandroit/react-multiselect-dropdown
For new releases, documentation updates, and the Angular counterpart, visit:
https://alexandro.net
What the library provides
Stackline React Multiselect Dropdown is a React multiselect component designed for real application forms, dashboards, filters, and enterprise interfaces.
It supports:
- single-select and multi-select modes
- controlled and uncontrolled selection
- search and filtering
- select all and clear all behavior
- grouped options
- custom item and badge render functions
- a guided Slots API for replacing component structure without losing behavior
- headless hooks (
useMultiSelectDropdown,useMultiSelectState) for fully custom interfaces - a type-safe factory (
createMultiSelectDropdown<T>()) to bind the item type once - imperative
refmethods (open, close, focus, select all, clear) - lazy loading hooks and dynamic data
- five built-in skins (
classic,material,dark,custom,brand) - accessibility-focused keyboard navigation, focus handling, and a documented combobox/ARIA contract
- body-overlay positioning for dialogs and clipped containers
- versioned documentation per React major
A multiselect component can look simple from the outside, but once it is used across several forms and workflows, replacing it becomes expensive. It affects state shape, validation, events, styling, selected state, reset behavior, focus and keyboard behavior, and sometimes even backend query logic. The cost is rarely in the first screen. It is in the twentieth screen that quietly depends on the behavior of the first.
That is why a predictable API, accessibility, customization depth, and a clear versioning story matter more than a long feature list.
Why this package exists
There are many dropdowns and select components available for React, but the hard part in real applications is rarely the first implementation.
The hard part is keeping the component stable while React moves forward, while forms evolve, while UI layouts become more complex, while accessibility gets audited, and while a design system needs to take over rendering without rebuilding selection logic from scratch.
React 19 makes that especially relevant. The framework is pushing toward Server Components, form Actions, and the React Compiler, which changes how data arrives and how much manual wiring a component needs. A selection control should be able to live inside that world: take its data from a Server Component or an Action, run as a client component for the parts that genuinely need the browser, and stay out of the way of the compiler.
This package focuses on a practical set of goals:
- keep React 19 compatibility clear and validated, not optimistic
- keep a controlled, predictable API:
datain,selectedItemsbound, updates throughonChange - offer escalating layers of control, so a team only owns as much markup as it actually needs
- ship a documented keyboard and ARIA contract instead of leaving accessibility as an afterthought
- provide working examples that can be tested in the browser before installing
- keep the same behavior contract as the Angular line, so multi-stack teams get consistency
The intention is not to reinvent how selection works. The intention is to make a commonly needed UI component easy to adopt at any level of customization, and to keep it behaving the same way across React versions and across the Angular counterpart.
React 19 in context
It helps to be explicit about what React 19 changes, because it shapes how any selection component should behave in a 2026 codebase.
| Area | Before React 19 | React 19 |
|---|---|---|
| Async/form state |
useState + useTransition + manual pending/error handling |
Actions, with useActionState, useFormStatus, and useOptimistic
|
| Reading async data and context |
useEffect + state; context only via useContext
|
use() reads promises and context during render, and can be called conditionally |
| Refs on function components |
forwardRef wrapper |
ref as a regular prop; forwardRef no longer required |
| Server rendering | SSR only | React Server Components as a production pattern, with a "use client" boundary |
| Memoization | manual useMemo / useCallback / memo
|
React Compiler inserts memoization automatically |
| Document head | third-party helmet libraries | native metadata hoisting for <title>, <meta>, <link>
|
| Types | @types/react@18 |
@types/react@19 |
There is a clear parallel with where Angular went in the same period. Angular's "signal-first" direction (signals, zoneless change detection, Signal Forms) and React's Actions, use(), and Compiler are different mechanisms aiming at the same outcome: more explicit reactivity, less boilerplate, and rendering that is easier to reason about. That convergence is exactly why a single shared behavior contract across both stacks is worth maintaining.
Where does a styled, controlled multiselect fit in React 19? It is, by nature, a client component. Selection, keyboard handling, focus management, and ARIA state all need the browser, so the component runs under "use client", and that is correct rather than a limitation. The boundary stays clean: a Server Component or an Action can fetch the option list, and the multiselect owns the interaction. For teams that want to push further — fit an RSC-driven design system, drop the bundled CSS, or own every element — the headless hooks provide the selection, filtering, grouping, and ARIA logic without any DOM or CSS, which is the React equivalent of a renderless control.
Installation
For React 19 projects, install the validated 19.x line with an exact version:
npm install @stackline/react-multiselect-dropdown@19.1.1 --save-exact
Pinning with --save-exact keeps compatibility predictable across local environments, CI pipelines, and production builds. The styled component ships its own styles and injects them at runtime; the headless hooks inject no CSS and let your application own the markup and styling.
Unlike the Angular 22 line, which deliberately loosens its peer range to be install-ready for the next framework major, the React 19 line keeps a strict peer range of >=19.0.0 <20.0.0. React's release cadence is slower and less calendar-driven than Angular's, so there is no fixed next-major date to prepare for; bounding the package to the tested React major is the more honest default here. The package was validated against React 19.2.4.
Setup
There is no module to register. React setup is three small steps.
1. Import the component
import { MultiSelectDropdown } from '@stackline/react-multiselect-dropdown';
import type {
DropdownSettings,
MultiSelectDropdownHandle
} from '@stackline/react-multiselect-dropdown';
2. Keep selection in React state
const [selectedCountries, setSelectedCountries] = useState<Country[]>([]);
3. Pass a stable settings object
const settings = useMemo<DropdownSettings<Country>>(
() => ({
text: 'Select countries',
enableSearchFilter: true,
primaryKey: 'id',
labelKey: 'itemName',
badgeShowLimit: 3,
skin: 'classic'
}),
[]
);
Wrapping the settings object in useMemo keeps its identity stable across renders, which avoids unnecessary work in the component. This is the React analogue of the Angular line's single settings object: most behavior is configured in one place rather than spread across props.
Basic usage
A typical implementation starts with a list of items, selected state, and a settings object.
import { useState, useMemo } from 'react';
import {
MultiSelectDropdown,
type DropdownSettings
} from '@stackline/react-multiselect-dropdown';
type Country = {
id: number;
itemName: string;
capital: string;
region: string;
};
const countries: Country[] = [
{ id: 1, itemName: 'Brazil', capital: 'Brasilia', region: 'South America' },
{ id: 2, itemName: 'Canada', capital: 'Ottawa', region: 'North America' },
{ id: 3, itemName: 'Portugal', capital: 'Lisbon', region: 'Europe' },
{ id: 4, itemName: 'United States', capital: 'Washington, DC', region: 'North America' },
{ id: 5, itemName: 'Argentina', capital: 'Buenos Aires', region: 'South America' },
{ id: 6, itemName: 'Mexico', capital: 'Mexico City', region: 'North America' }
];
export function CountrySelector() {
const [selectedCountries, setSelectedCountries] = useState<Country[]>([countries[1]]);
const settings = useMemo<DropdownSettings<Country>>(
() => ({
singleSelection: false,
text: 'Select countries',
selectAllText: 'Select all',
unSelectAllText: 'Clear all',
enableSearchFilter: true,
searchPlaceholderText: 'Search',
primaryKey: 'id',
labelKey: 'itemName',
badgeShowLimit: 4,
maxHeight: 260,
showCheckbox: true,
noDataLabel: 'No data',
skin: 'classic',
appendToBody: false
}),
[]
);
return (
<MultiSelectDropdown
data={countries}
selectedItems={selectedCountries}
onChange={setSelectedCountries}
settings={settings}
onSelect={(item) => console.log('selected', item)}
onDeSelect={(item) => console.log('removed', item)}
onSelectAll={(items) => console.log('selected all', items)}
onDeSelectAll={(items) => console.log('cleared all', items)}
/>
);
}
The controlled contract is the core idea: you hold the selected array, the component renders from it, and every change comes back through onChange. Note the skin key: the React line selects styling through skin. The older theme key still works as a legacy alias, but new code should use skin.
Customization paths
This is the part where React idioms shine, and it is the React equivalent of "use the right amount of control." Instead of one component with escape hatches, the package exposes escalating layers so a team owns only as much markup as it actually needs.
| Layer | Best for | What you own |
|---|---|---|
<MultiSelectDropdown /> |
Fast forms, filters, dashboards, admin screens | Data, selected state, settings, events, optional render functions |
createMultiSelectDropdown<T>() |
Binding the item type once for a feature or design-system wrapper | A typed component family, typed settings, typed slots, typed hooks |
slots |
Custom HTML around proven component behavior | Specific DOM pieces (trigger, badges, menu, search, groups, options, footer) |
useMultiSelectDropdown |
Fully custom interfaces and design systems | All markup and CSS, while the package provides state, ARIA prop getters, keyboard flow, grouping, callbacks |
useMultiSelectState |
Advanced state engines or an existing combobox shell | Every element, every event binding, all visual behavior |
For most teams, start with the component. Reach for slots when the component works but your layout needs a different shell. Reach for the headless hooks when the application must own the whole combobox contract. The important point is that selection, filtering, keyboard, focus, ARIA, body-overlay positioning, and async behavior stay handled by the package no matter which layer you choose.
Type-safe factory
Use createMultiSelectDropdown<T>() when a feature, package, or design-system wrapper should bind the item type once and reuse it across the component, settings, slots, and hooks. This is optional; the plain <MultiSelectDropdown /> API remains the fastest path for most screens.
import { useState } from 'react';
import { createMultiSelectDropdown } from '@stackline/react-multiselect-dropdown';
type Country = {
id: number;
itemName: string;
capital: string;
region: string;
};
const CountryMultiselect = createMultiSelectDropdown<Country>();
const countrySettings = CountryMultiselect.defineSettings({
text: 'Choose countries',
primaryKey: 'id',
labelKey: 'itemName',
searchBy: ['itemName', 'capital'],
groupBy: 'region',
enableSearchFilter: true,
badgeShowLimit: 2
});
const countrySlots = CountryMultiselect.defineSlots({
Option: ({ props, option, checkbox }) => (
<div {...props}>
{checkbox}
<strong>{option.item.itemName}</strong>
<small>{option.item.capital} - {option.item.region}</small>
</div>
)
});
export function CountryFilter({ countries }: { countries: Country[] }) {
const [selectedCountries, setSelectedCountries] = useState<Country[]>([]);
return (
<CountryMultiselect.Dropdown
data={countries}
selectedItems={selectedCountries}
onChange={setSelectedCountries}
settings={countrySettings}
slots={countrySlots}
/>
);
}
With the factory, primaryKey, labelKey, searchBy, and groupBy are checked against keyof Country. Typos such as labelKey: 'itemname' or searchBy: ['city'] fail at compile time instead of silently producing an empty search.
Settings-driven behavior
Most behavior is configured through the settings object instead of a long list of props.
Common settings include singleSelection, enableSearchFilter, searchBy, searchPlaceholderText, maxHeight, badgeShowLimit, limitSelection, disabled, groupBy, selectGroup, showCheckbox, noDataLabel, lazyLoading, labelKey, primaryKey, addNewItemOnFilter, clearAll, keyboard, autoPosition, position, skin (legacy alias: theme), appendToBody, and tagToBody.
Single selection without checkboxes
const settings: DropdownSettings<Country> = {
singleSelection: true,
showCheckbox: false,
enableCheckAll: false,
text: 'Select one country'
};
Selection limit
const settings: DropdownSettings<Country> = {
singleSelection: false,
limitSelection: 2,
badgeShowLimit: 2,
text: 'Select up to 2 countries'
};
Grouped data
const settings: DropdownSettings<Country> = {
groupBy: 'region',
selectGroup: true,
enableSearchFilter: true,
text: 'Select countries by region'
};
A single settings object is easy to standardize and share across an application. The most common cause of "the dropdown behaves slightly differently on this screen" is ten slightly different settings objects scattered across ten components; one shared object removes that drift.
Search and filtering
Search becomes important as soon as datasets grow.
enableSearchFilter turns search on, and searchBy restricts filtering to specific fields:
const settings: DropdownSettings<Country> = {
enableSearchFilter: true,
searchPlaceholderText: 'Search countries',
searchBy: ['itemName', 'capital']
};
This gives two useful modes: broad search across item properties, or controlled search over selected fields. Controlled search matters when items carry internal IDs, flags, or hidden metadata that should not match user input — a user typing "12" should not silently match an internal id of 12 unless you decided that on purpose.
Keyboard and accessibility — the combobox contract
This is the part of the React line that deserves the most attention, and it is the reference contract that the Angular line adopted.
Accessibility in a multiselect is a behavior contract, not a single attribute: what the keyboard does on the trigger versus inside the list, how focus moves after a selection, how selected chips are announced, and what screen readers report for each option. Version 19.1.1 tightens the details that usually break first in production forms.
| Behavior | Contract |
|---|---|
| Focus after selection or removal | Focus returns to search while the list stays open, or to the trigger when the list closes |
| Option selection state | Options expose matching aria-selected and aria-checked values |
| Backspace in search | Edits the query; with an empty query it does not remove selected values by default |
| Backspace/Delete on a focused badge remove button | Removes that selected badge |
| Escape | Closes the list without clearing selected values |
| Async add-item | Late async responses are ignored when a newer add request has already resolved |
| Async option refresh | Selected objects are merged back into the option source, so values are not lost when data refreshes |
| Keyboard navigation | Trigger ArrowDown/ArrowUp, option Home/End, and stable option ids keep focus and ARIA predictable |
Keyboard behavior is enabled by default and configurable through settings.keyboard when an application needs a stricter model:
const settings: DropdownSettings<Country> = {
text: 'Countries',
enableSearchFilter: true,
keyboard: {
space: true,
spaceOptionAction: 'toggle',
tab: true,
arrows: true,
escape: true,
backspaceRemovesLastWhenSearchEmpty: false,
deleteRemovesFocusedBadge: true
}
};
Set any key to false to disable that behavior. backspaceRemovesLastWhenSearchEmpty can be turned on for apps that want the legacy "empty search removes last badge" pattern (keyboard.backspace is still accepted as a deprecated alias). spaceOptionAction controls focused options: 'toggle' keeps focus on the current option, while 'toggle-and-next' toggles and advances to the next enabled option. escapeToClose: false is still supported and also disables keyboard.escape.
The edge cases that usually break accessibility — space inside search, tab stealing a selection, escape wiping a form field, a remote refresh dropping the user's selection — were handled deliberately. Because this is the same contract the Angular line follows, the keyboard and screen-reader behavior matches across both stacks.
Custom rendering and the Slots API
There are two levels of visual customization, and they map cleanly to two needs.
Use renderItem and renderBadge when you only need to replace the inner content of an option row or a selected chip:
<MultiSelectDropdown
data={countries}
selectedItems={selectedCountries}
onChange={setSelectedCountries}
settings={settings}
renderItem={(item, context) => (
<span>
<strong>{context.label}</strong>
<small>{item.capital}</small>
</span>
)}
renderBadge={(item) => <span>{item.itemName}</span>}
/>
Use the Slots API when you need to replace component structure — the trigger, menu, search shell, option row, badge, footer, and more. The rule is simple: spread the supplied props onto the element that plays that role. Those props carry the required refs, ARIA attributes, keyboard handlers, click handlers, ids, classes, and positioning styles, so you change the markup without losing the tested behavior.
import {
MultiSelectDropdown,
type MultiSelectDropdownSlots
} from '@stackline/react-multiselect-dropdown';
const countrySlots: MultiSelectDropdownSlots<Country> = {
Trigger: ({ props, children, state }) => (
<div {...props} className={`${props.className} country-trigger`}>
<span>
{state.selectedItems.length ? `${state.selectedItems.length} selected` : 'Choose countries'}
</span>
{children}
</div>
),
Badge: ({ props, item, removeButton }) => (
<span {...props} className={`${props.className} country-badge`}>
<strong>{item.itemName}</strong>
{removeButton}
</span>
),
Option: ({ props, option, checkbox }) => (
<div {...props} className={`${props.className} country-option`}>
{checkbox}
<span>
<strong>{option.label}</strong>
<small>{option.item.capital} - {option.item.region}</small>
</span>
</div>
),
MenuFooter: ({ props, state }) => (
<div {...props}>{state.filteredItems.length} visible options</div>
)
};
The full set of slots covers Root, Trigger, Value, Placeholder, SingleValue, BadgeList, Badge, BadgeLabel, BadgeRemove, Actions, OverflowCounter, ClearAll, Arrow, Menu, Toolbar, BulkActions, SelectAll, AddNewItem, Search, OptionList, Group, GroupHeader, GroupAction, Option, Checkbox, LoadingState, EmptyState, and MenuFooter. This is the React parallel to the Angular line's custom item and badge templates, taken further: in React you can replace almost any structural piece, not just the option and chip.
Headless usage
For the cases where slots are not enough, the package ships headless hooks. This is the React equivalent of the Angular renderless state helper: you write all of the HTML and CSS, and the package gives you state, ARIA prop getters, keyboard flow, grouping, limits, and callbacks.
useMultiSelectDropdown returns prop getters and state for a fully custom interface:
import { useState } from 'react';
import { useMultiSelectDropdown } from '@stackline/react-multiselect-dropdown';
export function HeadlessCountries() {
const [selectedItems, setSelectedItems] = useState([countries[0]]);
const dropdown = useMultiSelectDropdown({
data: countries,
selectedItems,
onChange: setSelectedItems,
settings: {
text: 'Choose countries',
enableSearchFilter: true,
searchPlaceholderText: 'Search country',
groupBy: 'region',
primaryKey: 'id',
labelKey: 'itemName',
badgeShowLimit: 2,
clearAll: true
}
});
return (
<div className="country-picker" {...dropdown.getRootProps()}>
<button className="country-trigger" {...dropdown.getTriggerProps()}>
<span>{dropdown.label}</span>
<strong>{dropdown.isOpen ? 'Close' : 'Open'}</strong>
</button>
<div className="country-chips">
{dropdown.selectedItems.map((item) => (
<span className="country-chip" key={dropdown.getItemKey(item)}>
{dropdown.getItemLabel(item)}
<button {...dropdown.getRemoveButtonProps(item)}>x</button>
</span>
))}
</div>
{dropdown.isOpen ? (
<div className="country-panel" {...dropdown.getListboxProps()}>
<input className="country-search" {...dropdown.getSearchInputProps()} />
{dropdown.visibleOptions.map((option) => (
<div
key={option.key}
{...dropdown.getOptionProps(option, {
className: option.selected ? 'country-option selected' : 'country-option'
})}>
<strong>{option.label}</strong>
<input type="checkbox" checked={option.selected} readOnly />
</div>
))}
</div>
) : null}
</div>
);
}
When you want the selection, filtering, and grouping engine without prop getters, use useMultiSelectState:
import { useMultiSelectState } from '@stackline/react-multiselect-dropdown';
const state = useMultiSelectState({
data: countries,
selectedItems,
onChange: setSelectedItems,
settings: {
primaryKey: 'id',
labelKey: 'itemName',
enableSearchFilter: true
}
});
The styled component is still available for drop-in usage. The hooks are for teams that want a headless ownership model, including design systems that need to fit React Server Component patterns or drop the bundled CSS entirely.
Forms and controlled state
React does not have ngModel or formControlName; a multiselect participates in a form through controlled state. Keep the selected array in state and derive validity from it:
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [selectedSkills, setSelectedSkills] = useState<Skill[]>([]);
const formIsValid = email.trim().length > 0 && selectedSkills.length > 0;
return (
<form onSubmit={(event) => event.preventDefault()}>
<input value={name} onChange={(event) => setName(event.target.value)} />
<input value={email} onChange={(event) => setEmail(event.target.value)} />
<MultiSelectDropdown
data={skills}
selectedItems={selectedSkills}
onChange={setSelectedSkills}
settings={skillSettings}
/>
<button type="submit" disabled={!formIsValid}>Submit</button>
</form>
);
A note on React 19 specifically: the framework's Actions and form hooks (useActionState, useFormStatus, useOptimistic) are now the recommended way to handle async submission. The selected array is plain React state, so it composes naturally with all of them — read selectedItems inside an Action, derive an optimistic update from it, or gate a submit button with useFormStatus. The component does not impose its own form abstraction, which keeps it compatible with whatever submission pattern a screen uses. The docs include both a "template-style forms" example and a "reactive forms" example, named to mirror the Angular line but implemented as ordinary React controlled state.
Events
The package exposes a full set of callbacks for selection workflows:
onChangeonSelectonDeSelectonSelectAllonDeSelectAllonOpenonCloseonScrollToEndonAddFilterNewItemonGroupSelectonGroupDeSelect
Selection events are useful when a change needs to trigger downstream behavior — refreshing a report, loading dependent filters, updating query parameters, sending analytics, enabling related controls, or validating dependent fields. The event names are kept stable across versions, so upgrading the React major does not silently break the handlers you already wrote.
Ref methods
For imperative control, the component exposes a typed handle through ref. In React 19 this works without forwardRef — ref is a normal prop on function components.
const dropdownRef = useRef<MultiSelectDropdownHandle<Country>>(null);
dropdownRef.current?.openDropdown();
dropdownRef.current?.closeDropdown();
dropdownRef.current?.focusSearch();
dropdownRef.current?.selectAll();
dropdownRef.current?.deSelectAll();
dropdownRef.current?.clearSelection();
This is handy for wizard-style flows, "clear filters" buttons elsewhere on the page, or opening the dropdown in response to an external event.
Lazy loading and dynamic data
Large datasets are where simple dropdowns start failing. Enable lazy loading through settings and append more rows when the list reaches the end:
const settings: DropdownSettings<Person> = {
text: 'Select people',
enableSearchFilter: true,
lazyLoading: true,
labelKey: 'name',
primaryKey: 'id',
maxHeight: 140
};
<MultiSelectDropdown
data={people}
selectedItems={selectedPeople}
onChange={setSelectedPeople}
settings={settings}
onScrollToEnd={() => setPeople((current) => current.concat(loadMorePeople()))}
/>
This matters for users, customers, products, permissions, locations, departments, catalogs, tags, and reporting dimensions. The docs also include a long-list keyboard-scroll example for large in-memory lists, alongside lazy loading and remote-data examples.
One detail worth calling out for async data: selected objects are preserved across asynchronous refreshes. When a remote call replaces the option list, previously selected items are merged back into the source instead of being silently dropped because the array reference changed. That is exactly the kind of bug that is invisible in a small demo and painful in production, and it is part of the shared contract with the Angular line.
Dialogs and overflow containers
A dropdown that works on a simple page can behave differently inside dialogs, modals, drawers, dashboard cards, table filters, and scrollable containers.
Use appendToBody: true or tagToBody: true when the dropdown sits inside a container that sets overflow: hidden or overflow: auto:
const settings: DropdownSettings<Country> = {
text: 'Dialog dropdown',
enableSearchFilter: true,
skin: 'material',
appendToBody: true,
tagToBody: true,
autoPosition: true
};
With body overlay enabled, the open panel renders against document.body, stays aligned to the original trigger, is sized to the trigger, recalculates on open, scroll, resize, and content changes, and is cleaned up when the dropdown closes or the component unmounts. autoPosition: true treats position as a preferred direction: the menu opens upward only when there is meaningfully less room below and enough room above; otherwise it opens below and shrinks the scrollable list height to stay visible without covering the trigger.
Theming and skins
Styling is selected through settings.skin. The React line ships five built-in skins, which is a superset of the Angular line's classic and material.
| Skin | Usage |
|---|---|
classic |
Compact classic dropdown styling |
material |
Material-style rounded controls and chips |
dark |
Dark UI surfaces |
custom |
CSS-variable starter skin for custom projects |
brand |
Stackline brand skin |
Switching skins is just a state change:
setSettings((current) => ({ ...current, skin: 'material' }));
settings.theme is accepted as a legacy alias so existing code keeps rendering, but new usage should configure skin. The styled component injects its CSS at runtime; if you want full control, the custom skin is a CSS-variable starting point, and the headless hooks let you drop the bundled CSS entirely and style your own markup.
Live functional examples and StackBlitz playground
The React 19 documentation includes real interactive examples running in a live React environment, and the editable playground is a dedicated React 19 StackBlitz tied to the maintained repository, so it stays in sync with the latest source instead of spawning stale forks per example.
The examples cover, among others:
- basic usage
- keyboard contract
- ARIA state audit
- headless + ARIA
- state hook
- Slots API
- type-safe factory
- async object preservation
- single selection
- search filter and search by property
- custom search from an API
- search and add a new item
- grouped options
- custom rendering (templating)
- template-style and reactive forms
- long-list keyboard scroll
- lazy loading from an API and remote data
- using the control in a list / loop
- using the dropdown inside a dialog
- multiple dropdowns on one page
- dynamic data loading
- methods and events
- disabled mode, selection limits, badge limits
- custom placeholder and styling
- body-overlay auto direction
These examples are not just static documentation. They let developers check the behavior directly in the browser before installing — how search behaves, how selected values are displayed, how empty states look, how keyboard navigation feels, and how the component behaves inside a dialog. For UI libraries, live examples are part of the trust model.
Official React 19 test matrix
The React 19 release was validated in a clean React 19.2.4 application using @stackline/react-multiselect-dropdown@19.1.1. The docs reuse the same examples from that test app, including keyboard navigation, focus, ARIA behavior, badge counters, responsive action buttons, scrollable lists, dialog-safe body overlays, the left-aligned and vertically centered placeholder, guided Slots API customization, headless custom HTML, and the combobox contract checks for Backspace, Escape, focused badge removal, focus, and option ARIA.
The same core scenarios are validated across the visual skins.
| # | Scenario | Main settings tested |
|---|---|---|
| 01 | Basic multi | { enableSearchFilter: true } |
| 02 | All selected badges visible | { badgeShowLimit: 10 } |
| 03 | Single selection | { singleSelection: true } |
| 04 | Search by fields | { searchBy: ['itemName', 'capital'] } |
| 05 | Grouped options | { groupBy: 'category', selectGroup: true } |
| 06 | Selection limit | { limitSelection: 2, badgeShowLimit: 2 } |
| 07 | Custom rendering |
renderItem and renderBadge
|
| 08 | Search and add item | { addNewItemOnFilter: true } |
| 09 | Disabled state | { disabled: true } |
| 10 | Controlled form validation | React state and derived validation |
| 11 | Long list with keyboard scroll | { maxHeight: 140 } |
| 12 | Local lazy loading | { lazyLoading: true } |
| 13 | Dialog and overflow container | { appendToBody: true, tagToBody: true } |
| 14 | Body overlay auto direction | { autoPosition: true, position: 'top' } |
| 15 | Ref methods |
openDropdown, closeDropdown, selectAll, clearSelection
|
| 16 | Slots API custom HTML |
slots.Trigger, slots.Option, slots.Search, slots.GroupHeader, slots.MenuFooter
|
Publishing the exact scenarios and settings that were tested is a small thing that pays off during adoption: instead of trusting a "tested on React 19" badge, you can see precisely which configurations were exercised, and reproduce them.
React version strategy
Each React major receives its own validated package line, locked to one React family. The compatibility table maps each package family to its React family, its peer range, and the tested release window.
| Package family | React family | Peer range | Tested release window |
|---|---|---|---|
| 19.x | React 19 only | >=19.0.0 <20.0.0 |
19.1.1 -> 19.2.4 |
| 18.x | React 18 only | >=18.0.0 <19.0.0 |
18.0.0 -> 18.3.1 |
| 17.x | React 17 only | >=17.0.0 <18.0.0 |
17.0.0 -> 17.0.2 |
This improves reproducible builds, compatibility validation, migration planning, and versioned-documentation consistency. The ranges are strictly bounded to the tested React major; the React line does not loosen its range the way the Angular 22 line does, because React's slower, less calendar-driven cadence means there is no fixed next-major date to prepare for. For React teams, predictable compatibility is usually more valuable than optimistic version ranges.
Adoption checklist
For teams adopting the React 19 line, I would use the following checklist.
1. Match the package family to your React major
Install the 19.x family for React 19 applications. Keep the package family aligned with the React major your app actually runs.
2. Install the package
npm install @stackline/react-multiselect-dropdown@19.1.1 --save-exact
3. Hold selection in React state
Use useState for the selected array and update it through onChange. Treat the component as fully controlled.
4. Use a stable settings object
Wrap settings in useMemo so its identity stays stable across renders.
5. Choose your customization layer
Start with <MultiSelectDropdown />. Move to slots when the layout needs a different shell, and to the headless hooks when the app must own the whole combobox. Use createMultiSelectDropdown<T>() if a feature or design-system wrapper should bind the item type once.
6. Configure styling through skin
Pick from classic, material, dark, custom, or brand. Move any theme usage to skin; theme still works as a legacy alias.
7. Verify keyboard and accessibility
Confirm that space inside search types a space, that tab does not select an option, that escape closes without clearing the field, and that options report consistent aria-selected and aria-checked. Configure settings.keyboard if your app needs different behavior.
8. Validate real UI scenarios
Test inside modals, scrollable containers, dashboard cards, table filters, and forms with validation. Use appendToBody: true (or tagToBody: true) with autoPosition for dialogs and overflow-clipped containers.
9. Review large-list and async behavior
Evaluate lazyLoading, the long-list keyboard-scroll example, maxHeight, onScrollToEnd, server-side filtering, and selected-object preservation across async refreshes.
10. Standardize settings across the app
Create shared settings presets for your design system to avoid slightly different dropdown behavior on different screens.
When this component is a good fit
This package is a strong fit when you need:
- React 19 compatibility, validated rather than assumed
- a controlled, predictable selection API (
datain,selectedItemsbound,onChangeout) - escalating customization: component, slots, headless hooks, and a typed factory
- search, select-all, grouping, and selection limits
- a documented keyboard and ARIA combobox contract
- custom item and badge rendering, plus structural slots
- imperative
refmethods for open, close, focus, select all, and clear - five built-in skins and full headless styling control
- lazy loading and object preservation for large async lists
- live examples and a StackBlitz playground to evaluate before adoption
- behavioral consistency with the Stackline Angular line for multi-stack teams
It is especially useful for React applications where predictable selection behavior and accessibility matter, and where a design system wants to own rendering without rebuilding selection logic.
It is probably not the right fit if you want a control built natively around Server Components with no client interactivity. A rich combobox is inherently a client component, and that is correct rather than a limitation. If you want to fit an RSC-heavy design system, the right path is the headless hooks: let a Server Component or an Action provide the data, and use the hook to own the interactive client part. Being clear about that boundary is part of the package being trustworthy.
Final thoughts
A good UI component is not only about what appears on the screen.
In real React projects, the important questions are practical: Does it keep selection state predictable? Does it preserve state across async refreshes? Is it accessible by keyboard and to screen readers? Can a design system customize it at the depth it needs? Are the examples reproducible? Is React compatibility clearly validated?
Stackline React Multiselect Dropdown for React 19 focuses on those concerns. The package includes search, grouping, select-all support, custom rendering, a guided Slots API, headless hooks, a typed factory, imperative ref methods, lazy loading, five skins, body-overlay positioning, and live React 19 examples running in a real application environment.
It is also built to be coherent with the Angular line. The combobox and ARIA contract, the skins, the headless philosophy, the per-major versioning, and the live-example model are shared across both ecosystems — with this React line serving as the reference implementation of the shared contract. For teams that run React and Angular side by side, that means a multiselect that behaves the same way in both, which is usually worth more than any single feature.
For new releases, documentation updates, and the Angular counterpart, visit:
Links
npm:
https://www.npmjs.com/package/@stackline/react-multiselect-dropdown
Documentation (React 19):
https://alexandro.net/docs/react/multiselect/react-19/#/basic
Live demo / interactive playground (StackBlitz):
https://stackblitz.com/github/alexandroit/stackline-react-multiselect-react-19?file=src%2Fexamples%2Fbasic%2Fbasic.component.tsx&startScript=start&initialpath=%2Fbasic
GitHub:
https://github.com/alexandroit/react-multiselect-dropdown
All React lines (React 17 to React 19):
https://alexandro.net/docs/react/multiselect/

Top comments (0)