Tag Input

Free

9 premium multi-select tag-input variants: default, avatar, grouped, creatable, colors, token limit, async search, inline and overflow.

</>sv-tag-input tagsmultiselectinput
import { SvTagInputComponent } from '@styloviz/tag-input';

Demo & usage

Basic usage

Usage example — open the gallery for the full interactive demo.

An interactive demo for this component lives in the gallery.

Open live demo

More examples

Compact size

example.htmlAngular
<sv-tag-input [options]="options" [(values)]="tags" size="sm" />

Async search (Pro)

queryChange is debounced (asyncDelay); fetch results and feed them back through [asyncOptions]. Async search ships in the Pro package — the tag is `sv-tag-input-pro`.

example.htmlAngular
<sv-tag-input-pro
  variant="async-search"
  [(values)]="tags"
  [asyncOptions]="results"
  (queryChange)="search($event)" />

Reactive forms (formControlName)

Implements ControlValueAccessor, so it binds to reactive and template-driven forms. The control value is a string[]; formControl.disable() disables the field.

example.htmlAngular
// form = new FormGroup({ tags: new FormControl<string[]>(['ng', 'ts']) });
<sv-tag-input formControlName="tags" [options]="options" label="Skills" />

<!-- or template-driven -->
<sv-tag-input [(ngModel)]="tags" [options]="options" />

When to use

  • Capturing multiple free-form or suggested tokens (tags, recipients, keywords).
  • You want creatable entries, token limits or async suggestions.
  • For a single choice from a list, use Select Dropdown or Autocomplete.

Async states

None — this component has nothing to load, so it has no loading, empty or error state to drive.

Inputs

NameTypeDefaultDescription
valuesstring[] (two-way)[]Current tags (two-way).
optionsTagOption[][]Suggestion options offered as the user types.
compareWith((a: string, b: string) => boolean) | nullnullWhat "the same tag" means. Defaults to strict equality on `value`. ⚠️ FREE, NOT PRO. `check:parity` flagged it as an undeclared Pro-only member and was right to: knowing whether two values are the same tag is correctness, not a premium feature, and a free control that silently duplicates a selection is broken rather than merely basic.
sizeTagInputSize'md'Field size: sm, md or lg.
labelstring''Field label.
placeholderstring'Add tags…'Placeholder shown in the input.
hintstring''Helper text shown below the field.
errorMessagestring''Error message; sets the field to an error state when present.
serverErrorstring | { readonly message: string } | nullnullA validation failure reported by the **server** — the 422 that comes back after submit, not something the client could have known. It is a separate input from `errorMessage` because it has a different lifetime, not because it needs a second way to show a string. A server error is a statement about *the value that was submitted*: the moment the user edits the field it stops being true, and this input expires itself rather than sitting there telling someone their new email is already taken. `errorMessage` has no such expiry — it is the host's to clear. While live it outranks `errorMessage`: it is the more specific fact, and it is the one the user has not seen yet. Typed structurally rather than as `SvDataError` so this stays dependency free — an `SvDataError` satisfies it unchanged.
disabledbooleanfalseDisable the field.
requiredbooleanfalseMark the field as required.

Outputs

EventPayload typeDescription
tagsChangeTagInputChangeEventEmitted with the change (added/removed tag + full list).
valuesChangestring[]Emits the new `values` whenever it changes — the output half of `[(values)]`.

<sv-tag-input-pro>

Also in this family — SvTagInputProComponent from @styloviz-pro/tag-input.

Async states

  • loading
  • error
  • retry

Inputs

NameTypeDefaultDescription
valuesstring[] (two-way)[]Two-way bound array of selected tag values.
optionsTagOption[][]Flat option list (used by all variants except grouped).
compareWith((a: string, b: string) => boolean) | nullnullWhat "the same tag" means. Defaults to strict equality on `value`. The defect this closes: IDENTITY WAS TWO DIFFERENT THINGS IN THE SAME COMPONENT. Selection compares `o.value === v`; `canCreate` compares lowercased LABELS. So a host whose ids differ only in case, or whose backend returns `'U1'` where the form holds `'u1'`, got a control that would neither de-duplicate nor resolve — and said nothing about either. ⚠️ IT COMPARES VALUES, NOT OPTIONS. The value is what the form holds and what a server sends back; the option may not exist yet when a value arrives (see `unresolvedTags`). A comparator over options would be uncallable in exactly the case that needs it.
groupsTagGroup[][]Grouped option list (used by grouped variant).
variantTagInputVariant'default'Visual variant — controls chip rendering and dropdown layout.
sizeTagInputSize'md'Field size.
labelstring''Visible label rendered above the field (hidden for inline variant).
placeholderstring'Add tags…'Placeholder shown in the search input when no tags are selected.
hintstring''Helper text rendered below the field.
errorMessagestring''Validation error message — shown in red below the field.
serverErrorstring | { readonly message: string } | nullnullA validation failure reported by the **server** — the 422 that comes back after submit, not something the client could have known. It is a separate input from `errorMessage` because it has a different lifetime, not because it needs a second way to show a string. A server error is a statement about *the value that was submitted*: the moment the user edits the field it stops being true, and this input expires itself rather than sitting there telling someone their new email is already taken. `errorMessage` has no such expiry — it is the host's to clear. While live it outranks `errorMessage`: it is the more specific fact, and it is the one the user has not seen yet. Typed structurally rather than as `SvDataError` so this stays dependency free — an `SvDataError` satisfies it unchanged.
disabledbooleanfalseDisables all interactions.
requiredbooleanfalseShows a required asterisk next to the label.
maxTagsnumber0Maximum number of tags that can be selected (0 = unlimited). The token-limit variant displays a visual progress counter.
asyncDelaynumber400Debounce delay in ms before `queryChange` is emitted for async-search. The parent component is expected to populate `asyncOptions` in response.
asyncOptionsTagOption[][]Options fed by the parent for the async-search variant. Update this whenever you receive results for the current query.
debounceMsnumber0Delay before `queryChange` fires, in ms. Spelled the same way as every other picker in the kit; falls back to `asyncDelay` when that is set.
serverSidebooleanfalseFetch options from the server in **any** variant, not just `async-search`. The supplied options are shown exactly as given.
minCharsnumber0Characters required before a lookup is sent. Below this the panel says so instead of showing a misleading empty list.
loadingbooleanfalseTrue while the host is fetching. Merged with the component's own in-flight state, so a real backend and the built-in delay render identically.
stalebooleanfalseThe results on screen answer an **older** query — a newer request is still in flight. Distinct from `loading`, which cannot separate the two cases it covers: the first fetch (nothing on screen — show a skeleton) and a refinement of results already visible (keep them, and say they are provisional). Blanking the panel on the second is the most jarring thing a picker can do; showing the old list with no indication is a quiet lie. Bind `svSuggestController(...).stale` from `@styloviz/data`, or `createRequestGuard().stale` from `@styloviz/core`. The results dim and report `aria-busy` while staying fully interactive.
errorSvDataError | string | nullnullFailure loading options, rendered inside the panel.

Outputs

EventPayload typeDescription
tagsChangeTagInputChangeEventFires every time the selection changes.
retryvoidThe host should re-run the failed option load.
queryChangestringFires (debounced) on every keystroke in the async-search variant. The parent should fetch results and set `[asyncOptions]` accordingly.
valuesChangestring[]Emits the new `values` whenever it changes — the output half of `[(values)]`.

Customization & theming

  • Nine `variant`s (default, with-avatar, grouped, creatable, with-colors, token-limit, async-search, inline, overflow) in three `size`s.
  • Provide `options` or `groups`; cap with `maxTags`; drive async suggestions with `asyncOptions` + `asyncDelay`; add `label`, `hint`, `errorMessage`, `required`.
  • Two-way bind `[(values)]` (string[]) or use a reactive control (CVA); react to `tagsChange` and `queryChange`. Fixed-position dropdown escapes overflow containers.

Best practices

  • Do: Offer suggestions via `options` so users reuse existing tags instead of inventing duplicates.
  • Do: Support Enter to add and Backspace to remove the last token.
  • Do: Bind with `formControlName` / `[(ngModel)]` for validation and dirty/touched tracking; `[(values)]` remains available for lightweight use.
  • Don't: Don't allow unbounded tokens where a limit makes sense — cap them with `maxTags`.
  • Do: Set `compareWith` when your ids are not byte-identical across the wire — a backend that returns `U1` where the form holds `u1` otherwise gets a control that neither de-duplicates nor resolves, and says nothing about either.
  • Do: Give options a `sublabel` when two of them can share a label. Two people called Alex Chen render as two identical chips otherwise, and removing one is a coin flip; with a sublabel the component adds it — but only to the chips that actually clash.
  • Do: Check `unresolvedTags()` after your options load. A selected value with no matching option is kept (dropping it would delete the user's choice because a list refreshed) and marked, so the chip shows as unknown instead of displaying a database id as though it were a name.
  • Do: Read `pasteReport()` if you need to react to a partial paste. Pasting 200 addresses into a limit of 50 adds 50 and reports 150 with reasons — over the limit, already in the list, matched more than one option, or not recognised — announced in a `role="status"` line. Silently taking 50 would look like it worked.

Accessibility

  • Correct ARIA combobox: `role="combobox"` sits on the `<input>` with `aria-expanded`, `aria-haspopup`, `aria-controls` and `aria-activedescendant`; the popup is a `role="listbox"` of `role="option"` rows (headers, loading and "no results" are presentational).
  • All option ids are generated per instance, so multiple tag fields on one page never collide; the input is named via `label` → `placeholder`.
  • Keyboard: arrows highlight, Enter adds, Backspace removes the last token; every chip exposes a labelled Remove button.
  • Implements ControlValueAccessor — works with `[(ngModel)]`, `[formControl]` and `formControlName`; `formControl.disable()` disables the field.

Feedback on Tag Input

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.