apx-dsv0.1Local renderer · live
K
Foundations4
  • Getting started
  • Theming
  • Templates
  • Icons
  • Avatar
  • Badge
  • DataGrid
  • Scheduler
  • Stat
  • Table
  • Timeline
  • TreeView
  • Accordion
  • Alert
  • EmptyState
  • Progress
  • Skeleton
  • Spinner
  • SplashScreen
  • Toast
  • ColorPicker
  • FileUpload
  • Form
  • Rating
  • TagsInput
  • Combobox
  • Field
  • Select
  • Toggle
  • Button
  • Calendar
  • Checkbox
  • DatePicker
  • Input
  • NumberInput
  • Radio
  • Slider
  • Switch
  • Textarea
  • AppShell
  • Div
  • Divider
  • Sidebar
  • Stack
  • Typography
  • Image
  • Breadcrumbs
  • Carousel
  • NavigationMenu
  • Pagination
  • Stepper
  • Tabs
  • Toolbar
  • CommandPalette
  • Confirm
  • Drawer
  • HoverCard
  • Menu
  • Modal
  • Popover
  • Tooltip
  • Icon
  • Card
  • PricingCard
60 componentsapx-ds/renderer
TagsInput
Variant↳ other

Form

TagsInput

Multi-value text input that produces an array of string tags from typing, paste, and (optional) autocomplete suggestions. Free-form companion to MultiCombobox.

TagsInput

A multi-value text input that turns typed text, pasted strings, and optional suggestions into an array of string tags. Companion to <MultiCombobox>:

<MultiCombobox><TagsInput>
Where do values come from?A constrained option listWhatever the user types (free-form)
New-value creationOptional (creatable)The default — every typed token becomes a tag
Suggestion listboxAlways visibleOptional; only shown when the user is typing
Typical use caseCustomer-facing selectorsPower-user input / admin forms / pickers

Both render badge chips inside a form-control shell, but the data flows are different. Pick the one that matches the UX intent.

Overview — pre-filled tags with suggestion list

Loading preview…
Overview.tsx

Why this exists

Hand-rolling a chips-input gets ten things wrong:

  • ❌ Press Enter on the last tag and the form submits.
  • ❌ Backspace deletes a tag without warning.
  • ❌ Paste a CSV of emails — it lands as one giant tag.
  • ❌ No keyboard way to remove a tag once you're past it.
  • ❌ Screen readers hear "removable" buttons but no announcement when you actually remove one.
  • ❌ Suggestions overlay never opens on small windows because of overflow.
  • ❌ Validation lives in the consumer and the field looks fine while it's wrong.

<TagsInput> ships every one of those fixed: a role="combobox" input with the W3C tags-input keyboard contract (commit/remove/tag-cursor), per-tag validate() that surfaces both visually (danger Badge color, title tooltip) and audibly (live-region announcement), paste-aware splitOn, optional maxTags enforcement, hidden inputs for native form submission, and an inline suggestion listbox that doesn't fight the page.

Anatomy

tsx
┌────────────────────────────────────────────────────────────┐
│  Tags *                                ← label              │
│  ─────────────────────────────────────────────              │
│  ╔════╗ ╔════╗ ╔════╗  type here…  3 / 10  ← field          │
│  ╚════╝ ╚════╝ ╚════╝                                       │
│  ─────────────────────────────────────────────              │
│  ┌───────────────────────────────────┐  ← suggestions       │
│  │ TypeScript                        │     listbox          │
│  │ TypeORM                           │     (inline)         │
│  └───────────────────────────────────┘                      │
│  Add at least one tag                  ← helper / error     │
└────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────┐
│  Tags *                                ← label              │
│  ─────────────────────────────────────────────              │
│  ╔════╗ ╔════╗ ╔════╗  type here…  3 / 10  ← field          │
│  ╚════╝ ╚════╝ ╚════╝                                       │
│  ─────────────────────────────────────────────              │
│  ┌───────────────────────────────────┐  ← suggestions       │
│  │ TypeScript                        │     listbox          │
│  │ TypeORM                           │     (inline)         │
│  └───────────────────────────────────┘                      │
│  Add at least one tag                  ← helper / error     │
└────────────────────────────────────────────────────────────┘
  • field — role="group" chip-row. Click anywhere → focuses the input.
  • input — role="combobox" with aria-expanded / aria-controls / aria-activedescendant.
  • tag chip — <Badge removable> by default; replace via renderTag.
  • suggestions — inline role="listbox" below the field; auto-opens when typing matches.
  • live region — off-screen aria-live="polite" that announces add / remove / invalid / duplicate.

Examples

Basic — controlled tag array, default outline shell

Loading preview…
Basic.tsx

With static suggestions

Loading preview…
WithSuggestions.tsx

Async suggestions (loadSuggestions + render)

Loading preview…
AsyncSuggestions.tsx

Per-tag validate() — invalid emails render in danger Badge

Loading preview…
EmailValidation.tsx

Paste a CSV / multi-line list — splitOn handles it

Loading preview…
PasteCsv.tsx

maxTags — input disables + placeholder swaps when reached

Loading preview…
MaxTags.tsx

showCount — '3 / 10' chip at the end of the field

Loading preview…
ShowCount.tsx

renderTag — opt out of the default Badge

Loading preview…
CustomRenderTag.tsx

renderSuggestion — rich rows for objects

Loading preview…
CustomRenderSuggestion.tsx

allowDuplicates — opt into repeated values

Loading preview…
AllowDuplicates.tsx

Sizes — sm / md / lg field; tagSize maps to Badge

Loading preview…
Sizes.tsx

Variants — filled / outline / ghost shell

Loading preview…
Variants.tsx

Disabled — no interaction, dimmed

Loading preview…
Disabled.tsx

Read-only — static display, no remove button

Loading preview…
ReadOnly.tsx

With label + description + helper

Loading preview…
WithLabel.tsx

Error state — required field, role='alert' helper

Loading preview…
ErrorState.tsx

Inside a form — hidden input per tag, FormData.getAll('tags')

Loading preview…
InForm.tsx

Props

PropTypeDefaultNotes
valuereadonly string[]—Controlled value. Pair with onChange.
defaultValuereadonly string[][]Uncontrolled initial value.
onChange(next, meta) => void—meta.action is 'add' / 'remove' / 'clear' / 'reject-…'. See TagsInputChangeMeta.
suggestionsreadonly T[]—Static suggestion list.
loadSuggestions(query, { signal }) => Promise<T[]>—Async fetcher; debounced + abortable via the Combobox-shared useDeferredFilter.
debounceMsnumber250Async debounce window.
minQueryLengthnumber0Suppress suggestions until the user types N chars.
filterSuggestions(items, query) => itemssubstringOverride the default filter for static suggestions.
getSuggestionValue(item) => stringString(item)Tag value for a suggestion item.
getSuggestionKey(item) => stringvalueReact key helper for non-string items.
renderSuggestion(item, { active, index, query }) => ReactNodevalue textCustom suggestion row.
splitOnstring[] | RegExp[' ', ',']Separators that commit pending input.
commitOnEnterbooleantrueEnter commits the pending input as a tag.
commitOnBlurbooleanfalseCommit when the input blurs (off by default — surprising in forms).
trimbooleantrueTrim whitespace before storing.
toLowerCasebooleanfalseLowercase before storing.
allowDuplicatesbooleanfalseWhether duplicate tags are accepted.
maxTagsnumber—Hard cap; input disables when reached.
validate(tag) => true | false | string—Per-tag validator; string return is a custom error message.
errorMessagestring'Invalid tag'Used when validate returns false.
renderTag(tag, { invalid, selected, removeProps, index, disabled }) => ReactNode<Badge>Replace the default chip.
showCountbooleanfalseRender "n / max" text at the end of the field.
emptyHintReactNode—Hint inside the field when no tags + no input.
label / description / helperText / error / required…—Standard form-field surface.
disabled / readOnlybooleanfalseDisable / static display.
namestring—Hidden-input name; one hidden input per tag for native form submission.
placeholderstringfrom i18nInput placeholder.
variant'filled' | 'outline' | 'ghost''outline'Field shell variant.
size'sm' | 'md' | 'lg''md'Field height + typography.
tagSize'xs' | 'sm' | 'md''sm'Badge size for default chips.
tagColorBadge color'neutral'Badge color for default chips.
tagVariantBadge variant'soft'Badge variant for default chips.
translationsPartial<TagsInputTranslations>EnglishReplace any subset of the default strings.

Keyboard

KeyAction
TypingAdd character to pending input.
Any separator in splitOnCommit pending input as a tag.
EnterCommit (when commitOnEnter).
Backspace at empty inputRemove the last tag.
ArrowLeft at input start (RTL: ArrowRight)Activate tag cursor on the last tag.
ArrowLeft / ArrowRight with cursor activeMove cursor between tags. Cursor past the last tag → focus input.
Delete / Backspace (cursor active)Remove the selected tag.
EscapeClose suggestions; deselect tag cursor.
ArrowDown / ArrowUp (suggestions open)Move highlighted suggestion.
Enter / Tab (suggestion highlighted)Commit the highlighted suggestion.
PasteMulti-token; runs splitTokens(pasted, splitOn) and commits in one batch.

A11y

  • input is role="combobox", aria-autocomplete="list", aria-expanded, aria-controls, aria-activedescendant — standard ARIA combobox pattern.
  • field is role="group"; clicking it focuses the input.
  • Tag chips are <span> by default (decorative); the remove button inside each chip is the actionable element with aria-label="Remove {tag}".
  • Live region (aria-live="polite") announces 'Added tag …' / 'Removed tag …' / '{tag} is already added' / 'Maximum N tags reached' / '{tag}: {error}'.
  • useFormFieldA11y (shared with <Input>, <Textarea>, <Select>, <Combobox>) wires label / description / helper / error IDs into aria-labelledby + aria-describedby.
  • axe-core: 0 violations in basic / labeled / max-reached / disabled / error / suggestion-open modes.

RTL

  • The chip row's flex-wrap mirrors natively under dir="rtl".
  • Arrow key semantics flip: ArrowRight activates the cursor in RTL.
  • Hidden input order matches the visual tag order (last-typed = last in value = last <input>).

Theming

<TagsInput> registers with the theme as TagsInput. Slot names:

wrapper · label · description · field · input · count · emptyHint · listbox · item · empty · helperText

ts
const theme = createTheme({
  components: {
    TagsInput: {
      defaultProps: { variant: 'filled', tagColor: 'primary' },
      styleOverrides: {
        field: 'gap-2 min-h-12',
        item: 'data-[active=true]:bg-primary/10',
      },
    },
  },
});
const theme = createTheme({
  components: {
    TagsInput: {
      defaultProps: { variant: 'filled', tagColor: 'primary' },
      styleOverrides: {
        field: 'gap-2 min-h-12',
        item: 'data-[active=true]:bg-primary/10',
      },
    },
  },
});

See also

  • <MultiCombobox> — constrained-list multi-select (every value must come from an option).
  • <Combobox> — single-value searchable select.
  • <Badge> — the chip primitive used for default tag rendering.
  • <Input> / <Textarea> — single-value text controls; same form-field surface.

Props

PropTypeDefaultDescription
allowDuplicatesbooleanfalseAllow duplicate tags.
commitOnBlurbooleanfalseCommit pending text when the input blurs.
commitOnEnterbooleantrueCommit on Enter.
debounceMsnumber250Debounce window for `loadSuggestions`.
defaultValuereadonly string[][]Uncontrolled initial value.
descriptionReactNode—Hint below the label.
disabledboolean—Removes interaction; field grays out.
emptyHintReactNode—Hint rendered inside the field when no tags + no input text yet.
errorReactNode—Bottom error. Sets `aria-invalid="true"` on the input + danger ring on the wrapper.
errorMessagestring'Invalid tag'Default error message when `validate` returns `false`.
filterSuggestions((items: readonly string[], query: string) => string[])—Override the default substring-match filter for static suggestions.
getSuggestionKey((item: string) => string)—Map a suggestion item to a stable React key. Defaults to the suggestion value.
getSuggestionValue((item: string) => string)—Map a suggestion item to its tag string. Defaults to `String(item)`.
helperTextReactNode—Bottom helper. Hidden when `error` is set.
labelReactNode—Visible label above the field.
loadSuggestions((query: string, ctx: { signal: AbortSignal; }) => Promise<string[]>)—Async suggestion fetcher (debounced + abortable).
maxTagsnumber—Hard cap on the number of tags.
minQueryLengthnumber0Min characters typed before suggestions appear.
namestring—Hidden-input name. Each committed tag posts a `<input type="hidden" name=…>` entry.
onChangeTagsInputChangeHandler—Fires on every committed mutation (add / remove / clear / reject).
placeholderstring—Placeholder for the inner `<input>`. Falls back to the i18n string.
readOnlyboolean—Static display: no add, no remove, no suggestions.
renderSuggestionTagsInputRenderSuggestion<string>—Render slot for each suggestion row. Defaults to the suggestion value as plain text.
renderTagTagsInputRenderTag—Custom tag renderer. Defaults to `<Badge removable>`.
requiredboolean—Sets `aria-required="true"` + cascades `required` onto the hidden inputs.
showCountbooleanfalseShow a "n / max" count chip to the right of the input.
sizeResponsiveValue<TagsInputSize>'md'Field height + typography.
splitOnRegExp | readonly string[][' ', ',']Separators that commit the pending input as tags.
styleCSSProperties——
suggestionsreadonly string[]—Static suggestion list. Use `loadSuggestions` for async.
sxSx—Theme-aware inline style object (resolves palette/spacing/radius tokens to CSS vars).
tagColorenum'neutral'Tag chip color (Badge palette).
tagSizeenum'sm'Tag chip size.
tagVariantenum'soft'Tag chip variant (Badge variant family).
toLowerCasebooleanfalseLowercase the tag before validating + storing.
translationsPartial<TagsInputTranslations>—Replace any subset of the English default strings.
trimbooleantrueTrim whitespace before validating + storing.
validateTagsInputValidator—Per-tag validator.
valuereadonly string[]—Controlled value. Pair with `onChange`.
variantResponsiveValue<TagsInputVariant>'outline'Visual variant of the wrapper (mirrors Input).