Multi Input

A field that collects several values as removable chips in one row.

Overview

MultiInput is one field that holds a list. Each committed entry becomes a chip with its own remove button, and the text input stays on the same row so the field grows downward as the list fills.

An entry is committed by Enter, by typing or pasting one of the separators, or by leaving the field. Entries are trimmed, blanks are dropped, and repeats are ignored unless allowDuplicates is set. Pass name to submit the list through a native form, or value and onValueChange to drive it from state.

Pass options to make it a multi-select: the field opens a dropdown of the remaining choices, narrows it as you type, and commits the highlighted one on Enter or a click. Add allowCustomValues={false} when only listed values may be entered — typed or pasted text then has to match an option's value or label, and anything else is rejected the way a failed validate is.

Preview

designengineering

Customize

Placeholder
Max
6
Allow Duplicates
Allow Custom Values
Empty Message
Disabled
Invalid

Installation

Add the component with the CLI. It copies the files in and installs anything they depend on.

npx vinyaas add multi-input

Usage

import { MultiInput } from "@/components/ui/multi-input"; export function Recipients() { return ( <MultiInput aria-label="Recipients" placeholder="Add a recipient…" defaultValue={["ada@vinyaas.dev"]} /> ); }

Examples

Default

Type an entry and press Enter, or end it with a comma. Backspace in an empty field removes the last value.

With a label

id points the Label at the inner text input.

formsaccessibility

Separators

Any listed character ends an entry, so a pasted list splits into values in one step.

Validation

A rejected entry stays in the field and marks it invalid, so the text can be corrected instead of lost.

Dropdown

options turns the field into a multi-select. The list opens on focus, narrows as you type, and hides values already chosen. An option can carry its own label and be disabled.

Listed values only

allowCustomValues={false} rejects anything the list does not hold, so the field accepts dropdown values and nothing else. Typed text still matches an option by its value or its label.

Limit

max caps the list. Entries past the limit stay in the field until a value is removed.

typescriptreact

Invalid and disabled

Both states match Input, so the field sits in a form unchanged.

one
onetwo

In practice

An invite form collects addresses with a comma or a space, validates each one as it is committed, and caps the list at ten.

ada@vinyaas.dev

1 of 10 invites. Separate addresses with a comma or a space.

API

Other attributes for the underlying element are passed through.

NameTypeDefaultDescription
valuestring[]—Controlled values. Pair it with onValueChange.
defaultValuestring[][]Starting values when the field is uncontrolled.
onValueChange(values: string[]) => void—Fires with the next list whenever a value is added or removed.
separatorsstring[][","]Characters that end an entry while typing or pasting. Enter and blur always commit.
placeholderstring—Shown while the field holds no values.
maxnumber—Maximum number of values. Entries past the limit stay in the field.
validate(value: string) => boolean—Return false to keep an entry in the field instead of committing it.
allowDuplicatesbooleanfalseKeeps repeated entries instead of dropping them.
options(string | MultiInputOption)[]—Turns the field into a multi-select. Typing filters the list and the field offers it in a dropdown.
allowCustomValuesbooleantrueWith options, set false to accept listed values only. Text matching no option is rejected.
emptyMessagestring"No matches."Shown when the dropdown has nothing left to offer.
disabledbooleanfalseDisables the text input and every remove button.
invalidbooleanfalseMarks the field invalid. A rejected entry also sets this on its own.
namestring—Submits one hidden input per value so the field works in a native form.
idstring—Applied to the inner text input so a Label can point at it.

Accessibility

  • Name the field with aria-label, or give it an id and point a Label at it.
  • Each remove button is a native button in the tab order, named Remove {value}, so the list can be emptied without a pointer.
  • Backspace in an empty field removes the last value — a shortcut, not the only way to remove one.
  • With options the text input is a combobox that owns the listbox: Arrow Down and Arrow Up move the highlight through the choices, Enter commits the highlighted one, and Escape closes the list without touching a surrounding dialog.
  • A rejected entry sets aria-invalid on the text input. Pair it with your own message when the reason is not obvious.