Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
Components
Buttons & Actions
ButtonButton GroupCopy ButtonDelete ButtonTheme ToggleToggleToggle Group
Inputs
Avatar UploadCalendarCheckboxColor PickerComboboxCountry SelectCron PickerCurrency InputDate PickerDate Range PickerDate Time PickerDuration PickerEmail InputEmoji PickerFile UploadFilter BuilderInputInput GroupListboxMasked InputMention InputMonth PickerMulti SelectNumber InputOTP InputPassword InputPhone InputRadio GroupRatingSearch InputSegmented ControlSelectSignature PadSliderSwitchTag InputTextareaTime PickerTimezone SelectTree Select
Forms
Address InputBooking WidgetCSV ImportFieldForm WizardInline EditInvoice line itemsRich Text EditorSchema Builder
Navigation
Back To TopBounce SidebarBreadcrumbCommand MenuGlobal SearchGooey NavHook SidebarNav MenuNav TabsPaginationProximity SidebarScroll ProgressSidebarStepsTable Of ContentsTabsTopbarUser MenuWorkspace Switcher
Overlays
Alert DialogContext MenuDialogDrawerDropdown MenuFamily DrawerHover CardPopoverShare DialogSheetShortcuts DialogTooltipTour
Feedback
AlertBannerCookie ConsentEmoji ReactionEmpty StateNotification BellNotification CenterProgressRoute ProgressSetup ChecklistSkeletonSpinnerToast
Data Display
Approval FlowAvatarBadgeCalendar ViewCode BlockComment ThreadContact cardCountdownDescription ListDiff ViewerFile ListFile managerFolderGanttJSON ViewerKbdLog ViewerMarkdownPresenceSchedulerTagTimelineTree View
Tables & Lists
Audit LogData TableDeal PipelineEditable GridKanbanListMembers tablePermissions MatrixTableTask ListVirtual List
Charts & Stats
Animated CounterArea chartBar chartDonut chartFunnel chartGaugeGitHub ActivityHeatmap calendarLeaderboardLine chartRadar chartRadial barSparklineStat CardTrackerTrend badge
Layout
AccordionApp ShellCardCollapsibleDetail LayoutEntity HeaderPage HeaderScroll AreaSeparatorSettings LayoutSplit ViewToolbar
Media
Audio PlayerCarouselCSV ViewerDOCX EditorDOCX ViewerExcel ViewerFile SystemImage compareImage CropperImage GalleryImage LightboxLocation MapPDF ViewerPowerPoint ViewerQR CodeStep PlayerVideo Player
AI Kit
Agent Status CardChat MessageChat ThreadChat WidgetConversation ListFluid OrbGrid RevealMatrix OrbModel PickerPrompt InputReasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage MeterVoice InputWorkflow Canvas
Text & Effects
Background PatternGravity LettersMarqueeTerminalText AnimateText Rotate
Mobile
Action SheetBottom Tab BarFABHaptic ButtonHero HeaderInstall PromptLarge Title HeaderNotification BannerOnboarding CarouselPicker WheelPull To RefreshSlide To ConfirmSticky CTA BarSwipe DeckSwipeable Tabs
Commerce
Cart drawerCategory pickerCheckout stepsCoupon inputFavorite buttonFilter panelListing cardListing gridOrder trackerPricePrice rangePricing tableProduct galleryQuantity stepperReviewsSeller card
Marketing Sections
App Download SectionBlog Cards SectionCTA SectionFAQ SectionFeature GridFeature ShowcaseHero SectionIntegrations SectionLogo CloudMarketing FooterMarketing HeaderMarketing SectionNewsletter SectionPricing SectionStats SectionSteps SectionTeam SectionTestimonials SectionTimeline SectionVideo Section
Blocks
Admin Dashboard PageAdmin ShellAI Chat PageAPI Keys PageAutomation Builder PageCheckout PageCRM Contact PageCRM Pipeline PageError PagesInbox PageInvoice PageKPI DashboardListing Detail PageLogin PageMarketplace Home PageMarketplace Search PageMarketplace ShellMobile App HomeOnboarding FlowOrder History PagePost Listing WizardPricing PageProfile PageRegister PageSettings PageUsers Admin Page
Templates
About PageAcademy PageAccessibility PageAffiliate PageAgency PageAI Product PageAppointment Booking PageBlog PageBlog Post PageBug Bounty PageCareers PageChangelog PageCommunity PageCompare PageContact PageCustomer Story PageCustomers PageDemo Booking PageDevelopers PageDocs Home PageDownload PageEnterprise PageEvents PageExperts PageFAQ PageFeature PageGlossary PageHelp Article PageHelp Center PageHome Services PageImpact PageIntegration Detail PageIntegrations PageJob Detail PageLanding PageLaunch PageLegal PageManifesto PagePortfolio PagePress PageProduct Tour PagePromo PageReal Estate PageReferral PageResearch Report PageRestaurant PageRoadmap PageROI Calculator PageSecurity PageSolutions PageStartups PageStatus PageStorefront PageSwitch PageTemplate Gallery PageTravel PageWaitlist PageYear in Review Page
Hooks & Utilities
AI ChatChart kitCommerceDate utilsFile exportFile typesFiltersForm validationHapticsInvoicingMotion presetsNavigationNumber formatRelative timeRomanian locationsSpreadsheetText matchTonesUIProvideruseClipboarduseConsentuseControllableStateuseCooldownuseDebounceuseKeyboardInsetuseLocaleuseLongPressuseMediaControlsuseMediaQueryuseOverflowItemsusePersistedStateuseScrollEdgesuseScrollStateuseSelectionuseShortcutuseStickToBottom
GalleryTheme

Inputs

Multi Select

Inputs

Multi Select React component for Tailwind CSS v4

Month PickerNumber Input
Live props
Field
label
description
error
required
invalid
disabled
readOnly
name
Content
placeholder
searchPlaceholder
emptyText
loadingText
sheetTitle
Appearance
display
maxVisibleChipsFit
showChipIcons
tone
color
highlightMatches
size
variant
radius
Layout
wrap
side
align
maxHeight360px
Behavior
chipRemovable
maxSelectedNo limit
selectAll
clearable
closeOnSelect
search
responsive
virtualThreshold100
creatable
fuzzy
State
loading
defaultOpen
Code
import { MultiSelect } from "@/components/ui/multi-select"

<MultiSelect />
  1. Home
  2. Components
  3. Inputs
  4. Multi Select
Pro

Multi Select React component for Tailwind CSS v4

v1.0.0·Added in 1.0.0

A React multi select: pick several options shown as chips with a +N overflow, with search, select all, a maximum and async options.

Click or tap the field to open the list under it; every row has a checkbox, and clicking a row toggles it while the list stays open (closeOnSelect closes it after each pick). The chosen options show as chips in the field: as many as fit on one line, measured as the field resizes, then a "+N" chip whose number crossfades; chips slide in and out as you pick. Each chip's x removes it without opening the list, and an x at the end of the field clears everything. The footer under the list shows the count ("3 selected", or "2 of 3 selected" with a maximum) and a Clear all button; at the maximum the unselected rows turn disabled. With selectAll a row on top toggles every visible option. Above 8 options (or with loadOptions, creatable or search="always") a search row sits on top and takes focus: typing filters, case and diacritics never matter and matched letters turn bold; Backspace in the empty search removes the last chip. display="count" shows "3 selected" instead of chips, display="text" the names joined and truncated, and wrap lets chips flow onto more lines while the field grows. Keyboard: Tab reaches the field (chip x buttons stay out of the tab order) and a click on its label focuses it. While closed, Enter, Space and ArrowDown open it, ArrowUp opens on the last option, Home and End open at the ends, and Backspace removes the last chip. While open, ArrowDown and ArrowUp move and wrap, PageUp and PageDown jump, Enter or Space toggle the highlighted row and keep the list open, Cmd/Ctrl+A selects all (when the list, not the search, has focus), Escape or Alt+ArrowUp close and keep focus on the field, Tab closes and moves on. On a phone the field opens a bottom sheet titled with the label, with 48px checkbox rows and a sticky footer: "Done (3)" commits the picks and "Clear all" empties the draft; dragging the sheet down, tapping outside or Escape discards the changes. With name, one hidden input per value submits (FormData.getAll), an empty required field blocks the submit and a form reset restores the default. With reduced motion chips and the list appear without sliding or scaling.

Multi Select is a Pro React component in the React input components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add multi-select, together with UIProvider, Listbox, Field, Drawer, Badge and 6 more and the npm packages @radix-ui/react-popover, motion and lucide-react.

Add to a project

Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup

npx wingo-ui@latest add multi-select

Multi Select is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans

Usage

Import MultiSelect from @/components/ui/multi-select.

import { useState } from "react"
import { Car, Building2, Smartphone, Dumbbell } from "lucide-react"
import { MultiSelect, type MultiSelectOption } from "@/components/ui/multi-select"

const CATEGORIES: MultiSelectOption[] = [
  { value: "real-estate", label: "Real estate", icon: <Building2 /> },
  { value: "vehicles", label: "Vehicles", icon: <Car /> },
  { value: "electronics", label: "Electronics", icon: <Smartphone /> },
  { value: "sports", label: "Sports", icon: <Dumbbell /> },
]

export function FollowedCategories() {
  const [categories, setCategories] = useState<string[]>(["vehicles"])
  return (
    <MultiSelect
      label="Followed categories"
      placeholder="Pick categories"
      options={CATEGORIES}
      value={categories}
      onValueChange={setCategories}
      maxSelected={3}
      selectAll
      name="categories"
      labels={{
        selected: (n) => `${n} picked`,
        limit: (n, max) => `${n} of ${max} picked`,
      }}
    />
  )
}

Props

  • options*
    MultiSelectItems

    Options and groups: { value, label, description?, icon?, badge?, disabled?, keywords?, textValue? } or { label, items }. The icon also shows inside the chip.

  • value
    string[]

    The chosen values, controlled. Values not among the options keep their chips (labels from the cache, else the value).

  • defaultValue
    string[]

    The starting values while uncontrolled (empty when omitted). A form reset returns to them.

  • onValueChange
    (value: string[]) => void

    Called with the new values after a toggle, a chip removal, select all, clear, or Done in the phone sheet.

  • labeldefault ""
    ReactNode

    The field label. A click focuses the trigger; plain text also titles the phone sheet and names the trigger with the count.

  • descriptiondefault ""
    ReactNode

    Helper text under the field, linked with aria-describedby.

  • errordefault ""
    ReactNode

    Replaces the description with an animated error and marks the field invalid.

  • requireddefault false
    boolean

    The red mark after the label, aria-required on the trigger; with name an empty field blocks the form submit.

  • invaliddefault false
    boolean

    The invalid look and aria-invalid without a message.

  • disableddefault false
    boolean

    Dims the field, never opens, chips are not removable and nothing is submitted.

  • readOnlydefault false
    boolean

    The chips stay visible and the field focusable, but it never opens and nothing can be removed or cleared.

  • placeholderdefault "Select options"
    string

    Shown in the field while nothing is chosen.

  • displaydefault "chips"
    "chips" | "count" | "text"

    How the value shows in the field: chips; "3 selected"; or the names joined and truncated.

  • maxVisibleChipsdefault 0
    number

    At most this many chips, then +N. 0 shows as many as fit on one line, measured on resize.

  • wrapdefault false
    boolean

    Chips wrap onto more lines and the field grows instead of collapsing into +N.

  • chipRemovabledefault true
    boolean

    Each chip has an x that removes it without opening the list; Backspace on the field removes the last one.

  • showChipIconsdefault true
    boolean

    The option's icon (an avatar, a flag) inside its chip.

  • maxSelecteddefault 0
    number

    The most options that can be chosen; at the limit the other rows turn disabled. 0 means no limit.

  • selectAlldefault false
    boolean

    A "Select all" row on top that toggles every visible option (respecting maxSelected).

  • clearabledefault true
    boolean

    An x at the end of the field that clears everything while something is chosen.

  • closeOnSelectdefault false
    boolean

    Closes the popover after every pick. The phone sheet always waits for Done.

  • searchdefault "auto"
    "auto" | "always" | "never"

    The search row: auto shows it above 8 options, with loadOptions or with creatable.

  • searchPlaceholderdefault "Search…"
    string

    The search row's placeholder.

  • emptyTextdefault "No results"
    string

    Shown when the search matches nothing (or there are no options).

  • tonedefault "neutral"
    "neutral" | "brand" | "success" | "warning" | "danger" | "info"

    The chips, the checkboxes and the sheet's Done button.

  • colordefault ""
    string

    Any CSS color for the chips and checkboxes; replaces the tone. Leave empty to use the tone.

  • loadingdefault false
    boolean

    Options are on their way: a spinner replaces the chevron and the list shows a loading row.

  • loadingTextdefault "Loading…"
    string

    The loading row's text and the spinner's accessible name.

  • highlightMatchesdefault true
    boolean

    Bold matched letters while searching.

  • leftIcon
    ReactNode

    A leading icon in the field.

  • sizedefault "md"
    "sm" | "md" | "lg"

    32 / 40 / 48px (md grows to 44px on touch) with 20 / 24 / 28px chips, 16px text on phones.

  • variantdefault "soft"
    "soft" | "outline" | "underline" | "ghost"

    The field look, the same as every other field.

  • radiusdefault "md"
    "none" | "sm" | "md" | "lg" | "full"

    Corner radius, scaled per size like the button; full also makes the chips pills. Ignored by underline.

  • namedefault ""
    string

    Renders one hidden input per value with this name, so FormData.getAll(name), required and form reset work.

  • id
    string

    The trigger's id (useId() by default); the label points at it.

  • fieldProps
    Partial<FieldProps>

    Anything else for the Field wrapper: optional, labelAction, orientation, labelWidth, hideLabel.

  • defaultOpendefault false
    boolean

    Starts with the list (or the sheet) open. Pass open and onOpenChange to control it.

  • open
    boolean

    Whether the list is open, controlled. It never opens while disabled or readOnly.

  • onOpenChange
    (open: boolean) => void

    Called when the list opens or closes.

  • responsivedefault true
    boolean

    A bottom sheet with 48px checkbox rows and a Done button below 768px instead of the popover.

  • sheetTitledefault ""
    string

    The phone sheet's title. Empty uses the label's text, else aria-label, else the placeholder.

  • sidedefault "bottom"
    "top" | "right" | "bottom" | "left"

    Where the list opens; it flips when there is no room.

  • aligndefault "start"
    "start" | "center" | "end"

    How the list lines up with the field.

  • maxHeightdefault 360
    number

    The list's maximum height in px (search and footer included), capped by the room on screen.

  • virtualThresholddefault 100
    number

    Above this many visible options only the rows in view render.

  • loadOptions
    (query: string, context: { signal: AbortSignal }) => Promise<MultiSelectItems>

    Async options for the debounced query; the previous request is aborted and results are cached per query.

  • debouncedefault 250
    number

    ms to wait after typing before loadOptions runs.

  • minQueryLengthdefault 0
    number

    loadOptions waits for this many characters.

  • creatabledefault false
    boolean

    A create row when the query matches nothing exactly; pair it with onCreate.

  • onCreate
    (query: string) => (string | void | Promise<string | void>)

    Creates an option from the query; a returned value is selected.

  • createLabel
    (query: string) => ReactNode

    The create row's text (default: Create "query").

  • filter
    boolean, or (option: MultiSelectOption, query: string) => score (number, null hides)

    Default true: true filters with text-match; false means the options are already filtered; a function scores (null hides).

  • fuzzydefault false
    boolean

    Fuzzy matching for the search.

  • renderOption
    (option, state: { selected, highlighted, disabled, ranges }) => ReactNode

    Replaces a row's content; the row element, its checkbox, aria and classes stay.

  • container
    HTMLElement | null

    Where the list or the sheet is portaled (document.body by default).

  • labels
    Partial<MultiSelectLabels>

    Every string: { selectAll, clearAll, done, selected(n), limit(n, max), more(n), remove(label), results(n), clearSearch, retry }. Defaults: "Select all", "Clear all", "Done", "3 selected", "2 of 3 selected", "+3", "Remove Auto", the announced result count, the search clear button and the retry button.

  • classNames
    Partial<Record<"field" | "label" | "description" | "error" | "counter" | "box" | "trigger" | "icon" | "chips" | "chip" | "overflow" | "placeholder" | "summary" | "clear" | "chevron" | "content" | "search" | "list" | "item" | "group" | "empty" | "footer", string>>

    Extra classes for the inner parts. box is the field box, trigger the button covering it, content the popover (or the sheet).

  • className
    string

    Merged onto the Field root when there is a label, description or error, otherwise onto the box.

  • ref
    RefObject<HTMLButtonElement | null> or (node) => void

    The trigger button; other props (onBlur, aria-*, data-*, form) go there too.

Examples

People with avatars

Avatars as option icons, shown in the rows and inside the chips; search by name or role.

A maximum

maxSelected={3}: at the limit the other rows turn disabled and the footer reads “3 of 3 selected”.

Count, text and chips

The same value shown as a count, as a truncated list of names and as chips with a measured +N.

Wrapping chips

wrap lets chips flow onto more lines and the box grows; each chip's x removes it without opening the list.

Groups and select all

Group labels, a disabled row and a “Select all” row that toggles every visible option, in the brand tone.

Async options

loadOptions against a fake API; results stay on screen while the next query loads and selected labels are kept.

Sizes, variants and colors

32, 40 and 48px, the four field looks, a tone and a custom color for chips and checkboxes.

Dependencies

@radix-ui/react-popovermotionlucide-reactui-configlistboxfielddrawerbadgebuttonspinnermotiontonesuse-media-queryuse-controllable-state

Files

  • components/ui/multi-select.tsx

FAQ

Is the React Multi Select component free?

The preview, props and docs are free; copying the source needs a Pro plan. See plans.

How do I install Multi Select?

Run npx wingo-ui@latest add multi-select. It also adds the Wingo UI items it builds on (UIProvider, Listbox, Field, Drawer, Badge and 6 more) and the npm packages @radix-ui/react-popover, motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/multi-select.

How does Multi Select behave on phones?

A bottom sheet with 48px checkbox rows and a Done button below 768px instead of the popover. The responsive prop controls this and is on by default.

See also

SelectListboxComboboxBadgeFieldDrawer

Used in

Country SelectRule Builder

More React input components

Number InputPassword InputPhone InputRadio GroupRatingSearch Input
All 40 React input components

Articles using this component

  • React Combobox: Async Search Without Race Conditions

    Component guides · Oct 16, 2026 · 10 min read

  • React Multi Select: Search, Chips, Select All and Mobile

    Component guides · Oct 16, 2026 · 10 min read

  • React Tag Input: Chips from Typing, Paste and Suggestions

    Component guides · Oct 13, 2026 · 8 min read

PreviousMonth PickerNext Number Input

Search preferences

We send you new listings every day

Real estateVehiclesElectronicsSports
Real estateVehiclesElectronicsSports+4

Selected: real-estate, vehicles, electronics, sports

display: chips · as many chips as fit · no limit · no “Select all” · stays open · search above 8 options · exact search · matches highlighted · virtualized above 100 · empty: “Select options” · phone: sheet “Followed categories” with “Done”