Designer view
Tag Input
An input that accepts multiple discrete values, rendering each committed value inline as a removable token next to a text input. Distinct from Combobox by emphasising visible accumulated state (the user sees all selected values as tokens), distinct from Select-multi by rendering tokens inline rather than as a count or comma-list, distinct from plain Input by structuring multi-value semantics. Used for email-recipient pickers, label/topic editors, filter-tag composers, multi-attribute search inputs.
When to use
Use
For multi-value inputs where the user benefits from seeing all selected values inline as removable tokens — email recipient pickers, label / topic editors, filter-tag composers, multi-attribute search inputs. The visible-state feedback is the canonical advantage over Combobox-multi (count-display) and Select-multi (no-typeahead).
Avoid
For single-value selection — that is `Combobox`, `Select`, or `Input`. For free-text without multi-value semantics — plain `Input`. For multi-select where the count is sufficient and inline tokens would crowd the layout — `Combobox[multi-select]` with count display. For very long lists (50+ tags) where wrapping breaks the layout — redesign with bulk-import flow.
Versus related
- combobox
`Combobox[multi-select]` displays the selection count in the input ("3 selected") rather than as inline tokens. `TagInput` shows the tokens. Use Combobox when screen real estate is constrained or the selection count is bounded; use TagInput when the user benefits from continuously seeing what they have selected.
- select
`Select[multi]` is a fixed-list multi-picker without text input; `TagInput` may accept free text (creatable variant) and renders selected values as inline tokens. Select is for bounded fixed lists; TagInput is for open-ended or visible-accumulation use cases.
- input
`Input` accepts free-text without structured multi- value semantics — the user types one value. `TagInput` structures multi-value input with explicit per-value tokens, paste-split, and per-value validation.
Figma anatomy
| Slot | Figma type | Hint |
|---|---|---|
root | frame | Auto-layout horizontal frame; wraps tags plus input across multiple rows when overflowing |
tag | instance | Tag/chip component instance with optional remove button; size and color from parent input |
tag-remove | instance | Icon button instance inside the tag; visibility bound to "removable" property |
input | instance | Text input component instance; min-width drives input visibility when many tags present |
placeholder | text | Placeholder text style; only visible in zero-tag empty-input state |
Token usage per slot
root- spacing
- padding
spacing.compact - gap
spacing.tight
- padding
- radius
- corner
radius.md
- corner
- color
- background
color.surface.bg - border
color.border.strong - ring
color.border.focus
- background
tag- spacing
- padding
spacing.tight - gap
spacing.tight
- padding
- radius
- corner
radius.sm
- corner
- color
- background
color.surface.sunken - foreground
color.text.primary
- background
- typography
- size
text.sm
- size
tag-remove- spacing
- padding
spacing.tight
- padding
- radius
- corner
radius.sm
- corner
- color
- foreground
color.text.muted - ring
color.border.focus
- foreground
input- spacing
- padding
spacing.tight
- padding
- color
- background
color.surface.bg - foreground
color.text.primary
- background
- typography
- size
text.md
- size
placeholder- color
- foreground
color.text.muted
- foreground
- typography
- size
text.md
- size
Figma ↔ Code property map
| Figma | Type | Code | Notes |
|---|---|---|---|
Variant | Variant | variant | Maps free-text / constrained / creatable. |
Size | Variant | size | sm / md / lg. |
Removable | Boolean | removable | Toggles the per-tag remove button slot. Read-only inputs (display historical tags) set false. |
Duplicates | Boolean | duplicates | Default false (reject duplicates). True for use cases where the same value may legitimately appear multiple times (rare; typically false). |
Max Tags | Variant | maxTags | none / low / medium / high. Drives the "+N more" affordance threshold and surfaces an inline error when the user attempts to exceed. |
Has Suggestions | Boolean | suggestions | For constrained / creatable variants. Toggles the suggestion listbox below the input. |
Placeholder | Text | placeholder | — |
Tag Label | Text | tagLabel | Per-tag content; typically the same as the underlying value but may differ (e.g. value=user-id, label=display-name). |
Tag Icon | Instance Swap | tagIcon | Optional leading icon per tag (avatar for user-tags, color swatch for label-tags). |
Motion
| Transition | Duration token |
|---|---|
tagEnter | motion.duration.fast |
tagExit | motion.duration.fast |
Responsive behaviour
| Breakpoint | Change |
|---|---|
breakpoint.sm | At and below, the input expands to full inline-size of its container (typical mobile pattern). Tags wrap to many rows naturally; consider switching `maxTags` to a lower threshold to surface a "+N more" affordance earlier. For constrained variant, the suggestion listbox may degrade to a bottom-anchored sheet (same pattern as Combobox at this breakpoint). |
breakpoint.md | Above this width, the input renders as authored — fixed inline-size with tags wrapping inside. |
Internationalisation
RTL · mirroring
Tag-list inline order reverses logically — first-added tag appears at the inline-start (visual right in RTL). ArrowLeft / ArrowRight keyboard navigation follows logical direction (ArrowLeft moves toward inline-start in both LTR and RTL; canonical model; in RTL contexts SR users may experience this as "moving toward the visual right", which is correct because tag order is also visually- right-first). The remove button on each tag moves to the inline-end of the tag (visual left in RTL).
Text expansion
Tag labels grow with translation; tags wrap to additional rows naturally. Long-text labels (DE / RU / FI) may force wider tags than designed; allow tag inline-size to grow intrinsically. Avoid fixed tag widths. The placeholder text expansion follows Input's expansion rules; consider longer placeholders ("Add tag…" → "Schlagwort hinzufügen…") forcing the input to a minimum inline-size sufficient to show full placeholder.
Variants, properties, states
Variants
Structurally different versions of the component.
free-textconstrainedcreatable Properties
The same component, parameterised.
| Property | Type |
|---|---|
removable | boolean |
duplicates | boolean |
size | sm | md | lg |
maxTags | none | low | medium | high |
States
Browser/user-driven (interactive) vs. app-driven (data).
| Kind | States |
|---|---|
interactive | hoverfocus-visibleactivedisabled |
data | emptyfilledbusyinvalid |
State transitions
| From | To | Trigger |
|---|---|---|
empty | filled | User commits the first tag — by typing into the input and pressing Enter, comma, or losing focus (consumer- configured commit triggers). The placeholder disappears; the tag-list begins rendering tokens. |
filled | empty | User removes the last tag (via the tag's remove button, Backspace at empty input, or programmatic clear). The placeholder reappears. |
filled | busy | For constrained / creatable variants with async suggestions, the user types; the suggestion request is in flight. The input shows a busy indicator; the tag list is unaffected. |
busy | filled | Async suggestions return; the input shows the listbox with results. |
filled | invalid | User attempts to commit a value that violates a constraint (already-present in `duplicates: false` mode, exceeds maxTags, fails a custom validator). The input surfaces an inline error and sets `aria-invalid="true"`. |
invalid | filled | User clears the in-flight value or successfully commits a valid alternative. `aria-invalid` is removed. |
Figma↔Code mismatches
- 01 Figma
Tags drawn as static text-with-border decorations next to an input
CodeTags as live DOM elements with remove buttons, accumulated into the form value
ConsequenceDesigners may treat tags as visual decoration. Implementations following the Figma file ship tags as styled spans with no remove affordance and no keyboard contract; users cannot remove tags except by retyping the entire value, and keyboard users cannot remove at all.
CorrectTags are interactive elements with remove buttons; the tag-list is part of the form value, not decoration. The Figma component must encode the remove affordance and the removable state; the canonical reference documents the keyboard contract (Backspace at empty input deletes last tag).
- 02 Figma
Tag remove button drawn but no keyboard navigation between tags
CodeArrowLeft / ArrowRight from input navigates between tags; Backspace deletes selected tag
ConsequenceDesigners draw the remove × on each tag without considering keyboard navigation. Developers shipping mouse-only remove break a11y for keyboard users — they must Tab through every tag's remove button, with no "select tag, then delete" canonical pattern.
CorrectDocument the canonical keyboard model: ArrowLeft from empty input selects the last tag (visual highlight), ArrowLeft / ArrowRight navigate between selected tags, Backspace / Delete removes the selected tag. The remove buttons remain mouse-targets; keyboard users use the navigation pattern.
- 03 Figma
Pasted comma-separated string drawn as a single tag
CodePasted "a, b, c" splits into three separate tags by canon
ConsequenceDesigners compose mock pastes as single tags; developers following the design ship literal-paste behaviour. Users pasting from spreadsheets or comma-separated lists get a single tag containing the entire pasted string; retyping each tag manually defeats the input's purpose.
CorrectDocument the canonical paste-split behaviour: pasted text containing commas, semicolons, or newlines splits into multiple tags on commit. Splitter characters are consumer-configurable but the default is `[,;\n]`.
- 04 Figma
Tag overflow drawn as truncating with ellipsis at the end of the input
CodeTags wrap to multiple rows; the input grows in block-size to accommodate
ConsequenceDesigners draw tag overflow as horizontal-truncate (the input has fixed inline-size and tags clip). Implementations following the design hide tags beyond a count; users cannot see what they have selected. Or developers ignore the design and ship multi-row wrap; the design and production diverge.
CorrectTags wrap naturally to multiple rows; the input grows in block-size. The wrapped layout is canonical because tags represent committed values that must remain visible. For space-constrained contexts, document `maxTags` plus a "+N more" affordance — but never silent clipping.
Common mistakes
#taginput-no-keyboard-token-removal
Tags cannot be removed by keyboard
Each tag has a × button reachable only via Tab through the entire tag list, plus there is no Backspace-at- empty-input canonical removal. Keyboard users must laboriously Tab through every tag's remove button; the a11y experience is significantly worse than mouse.
Implement the canonical keyboard model: Backspace at empty input selects (highlights) the last tag without removing; second Backspace removes it. ArrowLeft from empty input also selects the last tag for navigation. Once a tag is selected, ArrowLeft / ArrowRight navigate between tags; Backspace / Delete remove the selected tag.
#taginput-paste-no-split
Pasting comma-separated text creates a single tag
User pastes "alice@x, bob@y, carol@z" expecting three tags; gets one tag containing the entire string. Spreadsheet imports and quick-fill flows break.
On paste, split the pasted text on canonical separator characters (`,`, `;`, newline by default) and commit each segment as a separate tag. Validate each segment individually; segments that fail validation surface as a single "X invalid entries" error rather than aborting the entire paste.
#taginput-duplicates-silent
Duplicate tags accepted silently
`duplicates: false` is set but committing an already- present value silently no-ops. The input clears, but no error is shown; users do not know their attempted addition was rejected.
On duplicate-rejection, surface inline-error feedback: the input briefly highlights with an error tone, `aria-invalid="true"` is set, and a hidden status region announces "<value> already added". Avoid silent rejection; document the user-feedback contract canonically.
#taginput-no-aria-on-tags
Tags rendered as styled `<span>`s with no SR label structure
Tag list is a `<div>` of styled spans. SR users hear everything as plain text; the structure (this is a list of values, this is a remove button) is invisible. Worse, tag count and "X of Y selected" are not announced.
Render the tag list as a `<ul>` (or `<div role="list">`) with each tag as `<li>`. Each tag's accessible name is its label; the remove button is a real `<button>` with `aria-label="Remove <label>"`. SR navigates by list-item structure and hears each tag plus the remove option.
#taginput-tokens-clipped
Tag overflow silently clipped at input edge
Input has fixed inline-size; tags accumulate horizontally and clip at the inline-end. Users cannot see what they have committed beyond the visible tags; SR users hear everything but pointer users with vision rely on what they see.
Tags wrap to multiple rows by canon; the input grows in block-size. For dense layouts, document `maxTags` plus a "+N more" affordance — never silent clipping. Mature primitives (Radix, React Aria) wrap by default; implementations should not override.
Accessibility hints
| Slot | Accessibility hint | |
|---|---|---|
root | Root is a layout container; no required ARIA role. The focusable element inside is the `<input>` — clicking the root delegates focus to the input. Wrap with a `<label>` or pair via `aria-labelledby` so SR users hear a name when focus enters. | |
tag | Each tag is a list item inside an implicit list (the tag-list region of the input). For SR users, the tag's accessible name is its label; the remove affordance is an additional button labelled "Remove <tag label>". Tags themselves are not buttons — clicking the tag focuses or selects it, depending on consumer choice; clicking the remove affordance removes the tag. | |
tag-remove | Real `<button>` with accessible name "Remove <tag label>" (composed via aria-label). Activation removes the tag and moves focus to the next tag (or to the input if it was the last tag). Tab from the input lands here only when the input is empty and Backspace navigation has selected the tag for removal. | |
input | For free-text variant, plain `<input type="text">`. For constrained / creatable variants, the input is wired as a Combobox (`role="combobox"`, `aria-controls` referencing the suggestion listbox, `aria-activedescendant` for the highlighted suggestion). The accessible name comes from the wrapping label; the input's value is the in-flight text only — committed values live in the tag list. | |
placeholder | Placeholder is not the input's accessible name (the wrapping label is). Avoid placeholder-only labelling — when typing or after committing tags the placeholder disappears and SR users have nothing to identify the field from. |