react ~9 KB 0 deps v0.1.0 β†— GitHub β†—

@mshafiqyajid/react-multi-select

Headless hook and styled multi-select dropdown for React. Chips display, count badge, searchable, select-all with indeterminate state, grouped options, max selection limit, label/hint/error form control support, portal positioning, and full keyboard navigation.

Playground #

Props
TSX
import { MultiSelectStyled } from "@mshafiqyajid/react-multi-select/styled";
import "@mshafiqyajid/react-multi-select/styles.css";

<MultiSelectStyled
  options={options}
  value={value}
  onChange={setValue}
/>

Grouped options

Options with groups and a pre-selected state

Next.js

Form control

Select all that apply to your project.

Max selection

maxSelected=2 β€” try selecting a third option

Trigger modes

triggerMode="chips" β€” always shows chips

VueSvelteSolidJS

triggerMode="count" β€” always shows count badge

triggerMode="auto" maxChips=2 β€” chips up to 2, then count

Install #

npm install @mshafiqyajid/react-multi-select

Quick start #

import { MultiSelectStyled } from "@mshafiqyajid/react-multi-select/styled";
import "@mshafiqyajid/react-multi-select/styles.css";
import { useState } from "react";

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

const [value, setValue] = useState<string[]>([]);

<MultiSelectStyled
  options={options}
  value={value}
  onChange={setValue}
  placeholder="Select frameworks…"
/>

Grouped options #

Add a group key to any option β€” the dropdown renders section headers automatically.

const options = [
  { value: "react",  label: "React",   group: "Frontend" },
  { value: "vue",    label: "Vue",     group: "Frontend" },
  { value: "nextjs", label: "Next.js", group: "Full-stack" },
  { value: "nuxt",   label: "Nuxt",    group: "Full-stack" },
];

<MultiSelectStyled options={options} value={value} onChange={setValue} />

Max selection #

<MultiSelectStyled
  options={options}
  value={value}
  onChange={setValue}
  maxSelected={3}
  placeholder="Pick up to 3…"
/>

Trigger modes #

triggerMode="auto" shows chips up to maxChips (default 3), then switches to a count badge. Use "chips" to always show pills, or "count" to always show the badge.

// Auto (default): chips up to 3, then "N selected"
<MultiSelectStyled triggerMode="auto" maxChips={3} ... />

// Always chips
<MultiSelectStyled triggerMode="chips" ... />

// Always count
<MultiSelectStyled triggerMode="count" ... />

Form control #

<MultiSelectStyled
  options={options}
  value={value}
  onChange={setValue}
  label="Frameworks"
  hint="Select all that apply."
  required
/>

// With validation error
<MultiSelectStyled
  options={options}
  value={value}
  onChange={setValue}
  label="Frameworks"
  error="Please select at least one framework."
  invalid
/>

Custom option render #

<MultiSelectStyled
  options={options}
  value={value}
  onChange={setValue}
  renderOption={(option, { selected, focused }) => (
    <span style={{ display: "flex", gap: "0.5rem", width: "100%" }}>
      <span style={{ color: selected ? "#6366f1" : "#a1a1aa" }}>
        {selected ? "βœ“" : "β—‹"}
      </span>
      <span style={{ fontWeight: selected ? 600 : undefined }}>
        {option.label}
      </span>
    </span>
  )}
/>

Headless hook #

import { useMultiSelect } from "@mshafiqyajid/react-multi-select";

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

const {
  triggerProps,
  listboxProps,
  getOptionProps,
  isOpen,
  selectedValues,
  isAllSelected,
  isIndeterminate,
  selectAll,
  clearAll,
  filteredOptions,
} = useMultiSelect({ options });

return (
  <>
    <button {...triggerProps}>
      {selectedValues.length > 0 ? `${selectedValues.length} selected` : "Select…"}
    </button>
    {isOpen && (
      <ul {...listboxProps}>
        {filteredOptions.map((opt) => (
          <li key={opt.value} {...getOptionProps(opt)}>
            {opt.label}
          </li>
        ))}
      </ul>
    )}
  </>
);

API #

PropTypeDefaultDescription
optionsMultiSelectOption[]β€”Array of { value, label, group?, disabled? }
valuestring[]β€”Controlled selected values
defaultValuestring[][]Uncontrolled initial selection
onChange(v: string[]) => voidβ€”Called when selection changes
placeholderstring"Select options…"Trigger placeholder
searchablebooleantrueShow search input
searchPlaceholderstring"Search…"Search input placeholder
emptyTextstring"No results."Text when no options match search
showSelectAllbooleantrueShow select-all option
maxSelectednumberβ€”Maximum selections allowed
clearablebooleantrueShow clear-all button
triggerMode"chips" | "count" | "auto""auto"How selections appear in trigger
maxChipsnumber3Chip count before switching to badge (auto mode)
size"sm" | "md" | "lg""md"Height: 32 / 40 / 48px
tone"neutral" | "primary" | "success" | "danger""neutral"Color tone
disabledbooleanfalseDisable interaction
requiredbooleanfalseMark as required
invalidbooleanfalseShow invalid state
labelstringβ€”Label above trigger
hintstringβ€”Helper text below trigger
errorstringβ€”Error message (enables invalid state)
placement"auto" | "top" | "bottom""auto"Dropdown placement
renderOption(opt, state) => ReactNodeβ€”Custom option renderer
renderTrigger(selected) => ReactNodeβ€”Custom trigger content renderer
Edit this page on GitHub