npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@xsolla/xui-multi-select

v0.202.4

Published

A cross-platform multi-select control that lets users pick multiple options from a dropdown list.

Downloads

18,813

Readme

MultiSelect

A cross-platform multi-select control that lets users pick multiple options from a dropdown list.

A dropdown input that allows selecting multiple values simultaneously. Displays the current selections inside the field as text labels or removable tag chips, opens a ContextMenu with checkboxes when clicked, and supports an optional count tag when selections overflow. Built on the same trigger pattern as Select but with multi-value semantics.

When to use

  • When the user must choose more than one value from a predefined list — multiple tags, categories, permissions, countries, features
  • In filter bars where the user applies several filters simultaneously
  • In forms where a many-to-many relationship must be configured (e.g. assigning roles, selecting supported platforms)
  • When the full list of options must remain visible and re-selectable without navigating away

When not to use

  • When only one value can be selected at a time — use Select
  • When the number of selectable options is very small (2–4) and all options should be permanently visible — use Checkbox group or ToggleButtonGroup
  • When the user needs to type free-form tags or values not from a predefined list — use a tag input component
  • When the user needs to rank or order selections — use a dedicated sortable list

Content guidelines

  • Placeholder text — use a concise instructional phrase: "Select categories", "Choose platforms", "Pick permissions". Avoid generic "Select…" alone.
  • Option labels — keep each option label short (1–4 words). Use parallel structure across all options (all nouns or all short phrases). No trailing punctuation.
  • Option order — alphabetical or most common first. Do not randomise.
  • Count tag format — always use +N format (e.g. +3, +12). Do not spell out "3 more" inside a chip.
  • Clear button — enable for optional fields. Do not show on required fields — clearing all values on a required field immediately produces a validation error.
  • Field label — always provide a visible label above the field via FieldGroup.
  • Error messages — be specific:
  • "Please select at least one category" (required, empty on submit)
  • "You can select a maximum of 5 items" (max exceeded)

Behaviour guidelines

  • Multi-select model — multiple values can be selected independently. Selecting one value does not deselect others. Unchecking an option in the ContextMenu removes it from the field.
  • Individual removal (Tag variant) — when Variant=Tag and Remove button=true, clicking ✕ on a chip removes that specific value from the selection without reopening the dropdown. The ContextMenu reflects the change if it is open.
  • Clear all — clicking the clear button (✕, Clear button=true) removes all selections at once and returns the field to Fill=False.
  • Count tag — when selections overflow the visible content area, show a +N count tag. Clicking it opens the dropdown panel so the user can review all selected values.
  • Opening — clicking the field or pressing Enter / Space / ↓ when focused opens the ContextMenu. The field enters State=Focus; the chevron rotates upward.
  • Closing — the ContextMenu closes on: clicking outside, pressing Escape, or pressing Tab. Focus returns to the MultiSelect field.
  • Validation — validate on blur (field loses focus with the dropdown closed) or on form submit. Show State=Error with a message if the selection is invalid (e.g. required field with no selection, or max selection exceeded).
  • Disabled — State=Disable prevents opening the dropdown. Existing selections remain visible in muted style.
  • ContextMenu width — the ContextMenu panel must be at least as wide as the MultiSelect field. Never open a narrower panel.
  • Real-time update — the field content updates as the user checks/unchecks options. Do not batch updates to the field until the dropdown closes.

Accessibility

  • MultiSelect must have role="combobox" (or role="button") with aria-haspopup="listbox" and aria-expanded="true" / "false".
  • The field must have an accessible name via (from FieldGroup), aria-label, or aria-labelledby.
  • The selected values must be reflected in aria-label or as visually visible text so screen readers can announce the current state — e.g. aria-label="Categories: Design, Development, Marketing".
  • When Variant=Tag and Remove button=true, each chip's remove button must have aria-label="Remove [value name]" — e.g. aria-label="Remove Design".
  • The count tag (+N) must have aria-label describing the hidden count — e.g. aria-label="3 more selected items".
  • The clear button must have aria-label="Clear all selections".
  • When State=Error, the error message must be linked via aria-describedby and in an aria-live="polite" region.
  • The ContextMenu dropdown uses role="listbox" (or role="menu") with each option as role="option" / role="menuitemcheckbox" and aria-checked="true" / "false".
  • Keyboard navigation: Enter / Space / ↓ opens the dropdown; ↑ / ↓ navigate options; Space toggles the focused option; Escape closes without further changes; Tab closes and moves focus forward.
  • When a value is added or removed (via chip ✕ or from dropdown), announce the change via aria-live="polite": "Design added" or "Design removed".

Installation

npm install @xsolla/xui-multi-select

Imports

import { MultiSelect } from "@xsolla/xui-multi-select";
import type {
  MultiSelectProps,
  MultiSelectOption,
  MultiSelectValue,
  MultiSelectVariant,
  MultiSelectSize,
  MultiSelectState,
} from "@xsolla/xui-multi-select";

Quick start

const options = [
  { label: "React", value: "react" },
  { label: "Vue", value: "vue" },
  { label: "Angular", value: "angular" },
];

const [selected, setSelected] = useState<MultiSelectValue>([]);

<MultiSelect
  label="Frameworks"
  options={options}
  value={selected}
  onChange={setSelected}
  placeholder="Select frameworks"
/>;

External panel (B2B grouped select)

When the option list is rendered elsewhere (for example @xsolla/xui-b2b-group-select), set dropdownMenu={false} so the control does not open the built-in list. Wire the same value / onChange to both components; use onTriggerPress to toggle your panel, menuOpen for chevron/open styling, and menuMinWidth (default 540, aligned with GROUP_SELECT_MIN_PANEL_WIDTH) so the field matches the panel width.

import * as React from "react";
import { MultiSelect } from "@xsolla/xui-multi-select";
import {
  GroupSelect,
  GROUP_SELECT_MIN_PANEL_WIDTH,
  type GroupSelectGroup,
} from "@xsolla/xui-b2b-group-select";

const groups: GroupSelectGroup[] = [
  /* ... */
];
const flatOptions = groups.flatMap((g) =>
  g.items.map((it) => ({ value: it.id, label: it.label }))
);

export default function GroupedFieldShell() {
  const [value, setValue] = React.useState<string[]>([]);
  const [open, setOpen] = React.useState(false);

  return (
    <>
      <MultiSelect
        options={flatOptions}
        value={value}
        onChange={(v) => setValue(v.map(String))}
        placeholder="Select regions"
        size="sm"
        dropdownMenu={false}
        menuOpen={open}
        menuMinWidth={GROUP_SELECT_MIN_PANEL_WIDTH}
        onTriggerPress={() => setOpen((o) => !o)}
      />
      {open && (
        <GroupSelect groups={groups} value={value} onChange={setValue} />
      )}
    </>
  );
}

Add backdrop, click-outside, and Escape handling in your layout as needed (see Storybook).

API Reference

<MultiSelect>

| Prop | Type | Default | Description | | ------------------- | --------------------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | testID | string | — | Test ID for testing frameworks. On web this renders as data-testid; on React Native it renders as testID. | | options | MultiSelectOption[] | — | Available options. | | value | MultiSelectValue | [] | Selected values. | | onChange | (values: MultiSelectValue) => void | — | Fired when the selection changes. | | placeholder | string | 'Select' | Placeholder shown when empty. | | label | string | — | Label rendered above the control. | | size | 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' | 'md' | Control size. | | state | 'default' \| 'hover' \| 'focus' \| 'disable' \| 'error' | — | Forced visual state. | | disabled | boolean | false | Disable the control. | | errorMessage | string | — | Error message; also marks the control invalid. | | variant | 'tag' \| 'text' | 'tag' | How selected options are displayed. | | flexible | boolean | true | When true the control grows with content; otherwise fixed-height. | | removeTagsButtons | boolean | true | Show a remove button on each tag. | | extraClear | boolean | false | Show a clear-all button. | | maxHeight | number | 300 | Maximum dropdown height in pixels. | | searchable | boolean | false | Show a search input at the top of the built-in dropdown that filters options in memory by label. Web only. | | searchPlaceholder | string | 'Search' | Placeholder for the search input (used when searchable). | | noOptionsMessage | string | 'No results' | Message shown when the search filter matches no options. | | iconLeft | ReactNode | — | Icon rendered on the left of the control. | | iconRight | ReactNode | — | Icon on the right (overrides the default caret). | | dropdownMenu | boolean | true | When false, hides the built-in list and disables click-to-open; use with an external picker (e.g. GroupSelect) wired to the same value / onChange. | | onTriggerPress | () => void | — | When dropdownMenu is false: fired when the user activates the field. Typically toggles the external panel. | | menuOpen | boolean | false | When dropdownMenu is false: drives chevron direction and layering like the built-in open state. | | menuMinWidth | number | 540 | When dropdownMenu is false: field min-width in px (matches GroupSelect). Use 0 for no minimum. |

Inherits ThemeOverrideProps (themeMode, themeProductContext).

Types

type MultiSelectValue = (string | number)[];
type MultiSelectVariant = "tag" | "text";
type MultiSelectSize = "xs" | "sm" | "md" | "lg" | "xl";
type MultiSelectState = "default" | "hover" | "focus" | "disable" | "error";

interface MultiSelectOption {
  label: ReactNode;
  value: string | number;
  disabled?: boolean;
}

Examples

Sizes

const options = [
  { label: 'React', value: 'react' },
  { label: 'Vue', value: 'vue' },
  { label: 'Angular', value: 'angular' },
];

<MultiSelect options={options} size="xs" placeholder="Extra small" />
<MultiSelect options={options} size="sm" placeholder="Small" />
<MultiSelect options={options} size="md" placeholder="Medium" />
<MultiSelect options={options} size="lg" placeholder="Large" />
<MultiSelect options={options} size="xl" placeholder="Extra large" />

Text variant with clear-all

const options = [
  { label: "React", value: "react" },
  { label: "Vue", value: "vue" },
  { label: "Angular", value: "angular" },
];

const [selected, setSelected] = useState<MultiSelectValue>([]);

<MultiSelect
  options={options}
  value={selected}
  onChange={setSelected}
  variant="text"
  extraClear
/>;

Error state

const options = [
  { label: "Design", value: "design" },
  { label: "Engineering", value: "engineering" },
  { label: "Product", value: "product" },
];

const [skills, setSkills] = useState<MultiSelectValue>([]);

<MultiSelect
  label="Skills"
  options={options}
  value={skills}
  onChange={setSkills}
  errorMessage="Please select at least one skill"
/>;

Disabled

const options = [
  { label: "React", value: "react" },
  { label: "Vue", value: "vue" },
  { label: "Angular", value: "angular" },
];

<MultiSelect options={options} value={["react"]} disabled />;

Searchable

Enable searchable to show a search field at the top of the dropdown. It filters the options in memory by label as the user types, mirroring the Select component's searchable behavior. Customize the input hint with searchPlaceholder and the empty-result text with noOptionsMessage. Web only — the prop is ignored on native.

const options = [
  { label: "React", value: "react" },
  { label: "Vue", value: "vue" },
  { label: "Angular", value: "angular" },
  { label: "Svelte", value: "svelte" },
  { label: "Solid", value: "solid" },
];

const [selected, setSelected] = useState<MultiSelectValue>([]);

<MultiSelect
  label="Frameworks"
  options={options}
  value={selected}
  onChange={setSelected}
  searchable
  searchPlaceholder="Search frameworks"
  noOptionsMessage="No frameworks found"
  placeholder="Select frameworks"
/>;

Long option labels

Long, unbreakable option labels (URLs, IDs, tokens) wrap inside the dropdown menu instead of forcing a horizontal scrollbar; the menu width stays pinned to the control.

const options = [
  {
    label:
      "https://prototype.xsolla.dev/mini-apps/some-really-long-slug?token=abcdef0123456789",
    value: "url",
  },
  { label: "urn:xsolla:resource:0123456789abcdef0123456789abcdef", value: "id" },
  { label: "Short", value: "short" },
];

const [selected, setSelected] = useState<MultiSelectValue>([]);

<MultiSelect
  options={options}
  value={selected}
  onChange={setSelected}
  placeholder="Select options"
/>;

Accessibility

  • Selection is rendered as a checkbox list inside the dropdown.
  • The dropdown is keyboard navigable; selection state is announced to assistive technology.
  • An errorMessage marks the control as invalid for screen readers.
  • When searchable, the dropdown's search input exposes an aria-label matching searchPlaceholder; options filter in memory as the user types (web only).