Meet Frontfriend.Your brand. Your rules. Your agents, connected.Explore what’s new
DocumentationMultiSelect

MultiSelect

Select several options from a list, with optional creation of values the list does not contain.

Interactive preview

Rendered from the shared Frontfriend stories with React Base, the same preview Studio shows. Styling comes from your Design System; the code below is native to React Aria.

Installation

Complete project setup first. Your configuration must select react/aria and include your Design System ID.

pnpm exec frontfriend download multi-select -f react --primitive aria

Review dependencies reported by the downloader and keep your source changes in version control.

Usage

import { MultiSelect } from "@/components/ui/multi-select";
import * as React from "react";
const countryList = [
  { value: "italy", label: "Italy", keywords: ["roma", "it"] },
  { value: "uk", label: "United Kingdom", keywords: ["britain", "gb"] },
  { value: "france", label: "France", keywords: ["paris", "fr"] },
  { value: "germany", label: "Germany", keywords: ["berlin", "de"] },
  { value: "usa", label: "United States", keywords: ["america", "us"] },
];
const TRIGGER_WIDTH = "min-w-[260px] w-auto";
export default function MultiSelectExample() {
  const [value, setValue] = React.useState(["italy", "uk"]);
  return (
    <MultiSelect
      placeholder="Select..." maxCount={3} selectAll={false} clearable createNew={false} batchCreate={false} searchPlaceholder="Search..." closeOnSelect={false} options={countryList} className={TRIGGER_WIDTH}
      value={value}
      onValueChange={setValue}
    />
  );
}

Behavior

  • createNew and batchCreate are opt-in; batchCreate is inactive when createNew is false.
  • Batch creation uses comma, semicolon, tab, and newline separators. Paste with a separator is processed immediately; typed batches are processed on Enter or Create new.
  • normalizeNewValue applies only to new candidates before empty checks, validation, deduplication, and insertion.
  • validateNewValue is synchronous and runs per normalized candidate. onCreateNew runs only after a new option is accepted.
  • Valid unique candidates are retained when other batch candidates are invalid or duplicated; value bindings update once per batch.
  • Existing unselected options are selected rather than recreated; existing selected values and repeated normalized candidates count as duplicates.
  • options remains authoritative. Created selected values render with label ?? value and do not warn or become persistent searchable options until the consumer adds them to options.
  • Search is trimmed, case-insensitive, diacritic-insensitive substring matching over label, value, and keywords, with one target-native filtering pass and no public custom filter callback.
  • Filtered Select all adds missing visible options while preserving hidden selections; when every visible option is selected it removes only visible options.
  • Batch validation errors remain visible after partial success. Duplicate-only batches show duplicateErrorMessage; mixed successful batches report duplicates through announcements without a visual duplicate error.
  • The list exposes listbox semantics and rows expose option plus aria-selected. Internal checkboxes and icons are decorative; ariaLabel falls back to label.
  • Announcements have English defaults and accept partial semantic formatter overrides without hardcoded selected/not-selected text in option labels.
  • closeOnSelect closes only after selecting a previously unselected option; deselection keeps the popover open.

Examples

Standard selection

import { MultiSelect } from "@/components/ui/multi-select";
import * as React from "react";
const countryList = [
  { value: "italy", label: "Italy", keywords: ["roma", "it"] },
  { value: "uk", label: "United Kingdom", keywords: ["britain", "gb"] },
  { value: "france", label: "France", keywords: ["paris", "fr"] },
  { value: "germany", label: "Germany", keywords: ["berlin", "de"] },
  { value: "usa", label: "United States", keywords: ["america", "us"] },
];
const TRIGGER_WIDTH = "min-w-[260px] w-auto";
export default function MultiSelectExample() {
  const [selected, setSelected] = React.useState<string[]>(["italy", "uk"]);
  return (
    <MultiSelect
      placeholder="Select..." maxCount={3} selectAll={false} clearable createNew={false} batchCreate={false} searchPlaceholder="Search..." closeOnSelect={false} options={countryList} className={TRIGGER_WIDTH}
      value={selected}
      onValueChange={setSelected}
    />
  );
}

Controlled and uncontrolled values

import { MultiSelect } from "@/components/ui/multi-select";
import * as React from "react";
const countryList = [
  { value: "italy", label: "Italy", keywords: ["roma", "it"] },
  { value: "uk", label: "United Kingdom", keywords: ["britain", "gb"] },
  { value: "france", label: "France", keywords: ["paris", "fr"] },
  { value: "germany", label: "Germany", keywords: ["berlin", "de"] },
  { value: "usa", label: "United States", keywords: ["america", "us"] },
];
const TRIGGER_WIDTH = "min-w-[260px] w-auto";
export default function MultiSelectExample() {
  const [controlled, setControlled] = React.useState<string[]>(["italy"]);
  return (
    <div className="flex flex-col gap-4">
      <div className="flex items-center gap-2">
        <MultiSelect
          placeholder="Select..." maxCount={3} selectAll={false} clearable createNew={false} batchCreate={false} searchPlaceholder="Search..." closeOnSelect={false} options={countryList} className={TRIGGER_WIDTH}
          value={controlled}
          onValueChange={setControlled}
        />
        <button className="text-sm underline" onClick={() => setControlled([])}>
          Reset
        </button>
      </div>
      <MultiSelect
        placeholder="Select..." maxCount={3} selectAll={false} clearable createNew={false} batchCreate={false} searchPlaceholder="Search..." closeOnSelect={false} options={countryList} className={TRIGGER_WIDTH}
        defaultValue={["france"]}
        onValueChange={() => {}}
      />
    </div>
  );
}

Single create

import { MultiSelect } from "@/components/ui/multi-select";
import * as React from "react";
const countryList = [
  { value: "italy", label: "Italy", keywords: ["roma", "it"] },
  { value: "uk", label: "United Kingdom", keywords: ["britain", "gb"] },
  { value: "france", label: "France", keywords: ["paris", "fr"] },
  { value: "germany", label: "Germany", keywords: ["berlin", "de"] },
  { value: "usa", label: "United States", keywords: ["america", "us"] },
];
const TRIGGER_WIDTH = "min-w-[260px] w-auto";
export default function MultiSelectExample() {
  const [selected, setSelected] = React.useState<string[]>([]);
  const [created, setCreated] = React.useState<string[]>([]);
  return (
    <div className="flex flex-col gap-2">
      <MultiSelect
        placeholder="Select..." maxCount={3} selectAll={false} clearable createNew batchCreate={false} searchPlaceholder="Search..." closeOnSelect={false} options={countryList} className={TRIGGER_WIDTH}
        value={selected}
        onValueChange={setSelected}
        onCreateNew={(option) => setCreated((all) => [...all, option.value])}
      />
      <p className="text-sm">Created: {created.join(", ") || "—"}</p>
    </div>
  );
}

Normalized partial batch create

import { MultiSelect } from "@/components/ui/multi-select";
import * as React from "react";
const TRIGGER_WIDTH = "min-w-[260px] w-auto";
export default function MultiSelectExample() {
  const [selected, setSelected] = React.useState<string[]>([]);
  return (
    <MultiSelect
      placeholder="Add recipients..." maxCount={3} selectAll={false} clearable createNew batchCreate searchPlaceholder="Search..." closeOnSelect={false} options={[]} className={TRIGGER_WIDTH}
      value={selected}
      onValueChange={setSelected}
      normalizeNewValue={(value) => value.trim().toLowerCase()}
      validateNewValue={(value) => /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)}
      validationErrorMessage="Enter a valid email address"
      duplicateErrorMessage="Already added"
    />
  );
}

Label, value and keyword search

import { MultiSelect } from "@/components/ui/multi-select";
import * as React from "react";
const countryList = [
  { value: "italy", label: "Italy", keywords: ["roma", "it"] },
  { value: "uk", label: "United Kingdom", keywords: ["britain", "gb"] },
  { value: "france", label: "France", keywords: ["paris", "fr"] },
  { value: "germany", label: "Germany", keywords: ["berlin", "de"] },
  { value: "usa", label: "United States", keywords: ["america", "us"] },
];
const TRIGGER_WIDTH = "min-w-[260px] w-auto";
export default function MultiSelectExample() {
  const [selected, setSelected] = React.useState<string[]>([]);
  return (
    <MultiSelect
      placeholder="Select..." maxCount={3} selectAll={false} clearable createNew={false} batchCreate={false} searchPlaceholder="Search..." closeOnSelect={false} options={countryList} className={TRIGGER_WIDTH}
      value={selected}
      onValueChange={setSelected}
    />
  );
}

Filtered Select all merge and removal

import { MultiSelect } from "@/components/ui/multi-select";
import * as React from "react";
const countryList = [
  { value: "italy", label: "Italy", keywords: ["roma", "it"] },
  { value: "uk", label: "United Kingdom", keywords: ["britain", "gb"] },
  { value: "france", label: "France", keywords: ["paris", "fr"] },
  { value: "germany", label: "Germany", keywords: ["berlin", "de"] },
  { value: "usa", label: "United States", keywords: ["america", "us"] },
];
const TRIGGER_WIDTH = "min-w-[260px] w-auto";
export default function MultiSelectExample() {
  const [selected, setSelected] = React.useState<string[]>(["italy"]);
  return (
    <MultiSelect
      placeholder="Select..." maxCount={3} selectAll clearable createNew={false} batchCreate={false} searchPlaceholder="Search..." closeOnSelect={false} options={countryList} className={TRIGGER_WIDTH}
      value={selected}
      onValueChange={setSelected}
    />
  );
}

Accessible announcements and selected state

import { MultiSelect } from "@/components/ui/multi-select";
import * as React from "react";
const countryList = [
  { value: "italy", label: "Italy", keywords: ["roma", "it"] },
  { value: "uk", label: "United Kingdom", keywords: ["britain", "gb"] },
  { value: "france", label: "France", keywords: ["paris", "fr"] },
  { value: "germany", label: "Germany", keywords: ["berlin", "de"] },
  { value: "usa", label: "United States", keywords: ["america", "us"] },
];
const TRIGGER_WIDTH = "min-w-[260px] w-auto";
export default function MultiSelectExample() {
  const [selected, setSelected] = React.useState<string[]>([]);
  return (
    <MultiSelect
      placeholder="Select..." maxCount={3} selectAll clearable createNew={false} batchCreate={false} searchPlaceholder="Search..." closeOnSelect={false} options={countryList} className={TRIGGER_WIDTH}
      value={selected}
      onValueChange={setSelected}
      announcements={{ selected: (option) => `${option.label} selezionato` }}
      clearAriaLabel="Rimuovi tutte le selezioni"
    />
  );
}

contentClassName customisation

import { MultiSelect } from "@/components/ui/multi-select";
import * as React from "react";
const countryList = [
  { value: "italy", label: "Italy", keywords: ["roma", "it"] },
  { value: "uk", label: "United Kingdom", keywords: ["britain", "gb"] },
  { value: "france", label: "France", keywords: ["paris", "fr"] },
  { value: "germany", label: "Germany", keywords: ["berlin", "de"] },
  { value: "usa", label: "United States", keywords: ["america", "us"] },
];
const TRIGGER_WIDTH = "min-w-[260px] w-auto";
export default function MultiSelectExample() {
  const [selected, setSelected] = React.useState<string[]>([]);
  return (
    <MultiSelect
      placeholder="Select..." maxCount={3} selectAll={false} clearable createNew={false} batchCreate={false} searchPlaceholder="Search..." closeOnSelect={false} options={countryList} className={TRIGGER_WIDTH}
      value={selected}
      onValueChange={setSelected}
      contentClassName="max-h-64 shadow-lg"
    />
  );
}

Custom search and close on select

import { MultiSelect } from "@/components/ui/multi-select";
import * as React from "react";
const countryList = [
  { value: "italy", label: "Italy", keywords: ["roma", "it"] },
  { value: "uk", label: "United Kingdom", keywords: ["britain", "gb"] },
  { value: "france", label: "France", keywords: ["paris", "fr"] },
  { value: "germany", label: "Germany", keywords: ["berlin", "de"] },
  { value: "usa", label: "United States", keywords: ["america", "us"] },
];
const TRIGGER_WIDTH = "min-w-[260px] w-auto";
export default function MultiSelectExample() {
  const [selected, setSelected] = React.useState<string[]>([]);
  return (
    <MultiSelect
      placeholder="Select..." maxCount={3} selectAll={false} clearable createNew={false} batchCreate={false} searchPlaceholder="Find a country" closeOnSelect options={countryList} className={TRIGGER_WIDTH}
      value={selected}
      onValueChange={setSelected}
    />
  );
}

API reference

Frontfriend-owned props. Native HTML and inherited primitive props remain documented by React Aria.

Prop Type Default Description
animation number —
announcements Partial<MultiSelectAnnouncements> —
batchCreate boolean — Split one create into several values on comma, semicolon, tab or newline
clearable boolean —
clearAriaLabel string —
closeOnSelect boolean —
contentClassName string —
createNew boolean — Allow values absent from `options` to be created from the search box
createNewText string —
defaultValue string[] —
duplicateErrorMessage string —
emptySearchText string —
fullWidth boolean —
listAriaLabel string —
maxCount number —
modalPopover boolean —
moreBadgeSubtle boolean —
moreBadgeText string —
moreBadgeVariant BadgeVariant —
normalizeNewValue (value: string) => string —
onCreateNew (option: MultiSelectOption) => void —
onValueChange (value: string[]) => void —
options MultiSelectOption[] —
placeholder string —
searchAriaLabel string —
searchPlaceholder string —
selectAll boolean —
selectAllText string —
validateNewValue (value: string) => boolean —
validationErrorMessage string —
value string[] —

Configuration determines the actual classes and available styling hooks. See Theming & component configuration before introducing a repeated local override.