Appearance
Tags — the cross-framework headless token / tags input
Tags is a headless, fully-accessible tags / token input with no third-party engine behind it. It covers the whole behaviour surface: type-to-add with configurable delimiter keys, paste-to-bulk-add, Backspace-deletes-previous, dedup, per-token validation, a max cap, removable chips with labelled remove controls, a live token count, and the focus choreography. The same component ships for React, Vue, Svelte, Angular, Solid, and Lit.
The foundation is the platform itself: one native <input> for typing plus a row of removable chips. The browser keyboard, the clipboard (paste), and focus all come from the platform. The committed tokens are modelValue (the sole model: true prop), so the value is fully two-way bound; the only local state is the in-progress draft text in the input, a genuine UI buffer distinct from the committed list. Rozie owns the author-side API: the two-way r-model:modelValue, the commit / dedup / validate / cap logic, paste distribution, the Backspace behaviour, the focus choreography (via one container ref, never per-chip refs), and the token-themed skin.
Every visual value is a CSS custom property, so the input re-skins to any design system, with ready-made bridges for shadcn/ui, Material 3, and Bootstrap 5.
The @rozie-ui/tags packages
Tags ships as six pre-compiled, per-framework packages. Install the one for your framework; there is no build step and no Rozie toolchain to set up:
| Package | Install | README |
|---|---|---|
@rozie-ui/tags-react | npm i @rozie-ui/tags-react | react/README |
@rozie-ui/tags-vue | npm i @rozie-ui/tags-vue | vue/README |
@rozie-ui/tags-svelte | npm i @rozie-ui/tags-svelte | svelte/README |
@rozie-ui/tags-angular | npm i @rozie-ui/tags-angular | angular/README |
@rozie-ui/tags-solid | npm i @rozie-ui/tags-solid | solid/README |
@rozie-ui/tags-lit | npm i @rozie-ui/tags-lit | lit/README |
Each package carries only its framework peer (react + react-dom, vue, svelte, @angular/core + @angular/common + @angular/forms, solid-js, or lit + @lit-labs/preact-signals + @preact/signals-core).
Quick start
Two-way bind modelValue (a string[]) and type — press Enter or comma to commit a token. Paste a comma-separated list to bulk-add; Backspace in an empty input removes the last token:
rozie
<components>
{
Tags: './Tags.rozie',
}
</components>
<data>
{
skills: ['rozie', 'vue'],
}
</data>
<template>
<Tags
r-model:modelValue="$data.skills"
placeholder="Add a skill…"
ariaLabel="Skills"
:max="8"
@add="onAdd"
/>
</template>r-model:modelValue is Rozie's two-way bind: the consumer hands Tags an array, Tags writes a fresh array back on every add/remove, and the framework reconciler picks it up with no onChange → setState wiring. Because modelValue is the component's sole model: true prop, the Angular output additionally implements ControlValueAccessor, so a Tags is a form control ([formControl] / [(ngModel)] bind directly).
Custom chip rendering
Every chip is rendered through a scoped #tag slot whose params are { tag, index, remove }. The default fallback renders the built-in chip (a label + a labelled remove button); override the slot to render anything — a pill, an avatar, a status dot — and call remove() from your own control:
rozie
<template>
<Tags r-model:modelValue="$data.skills" ariaLabel="Skills">
<template #tag="{ tag, remove }">
<span class="my-pill">{{ tag }} <button type="button" @click="remove">×</button></span>
</template>
</Tags>
</template>On React the slot surfaces as a render-prop children callback — the one documented cross-framework slot divergence.
API
The full prop / event / handle / slot surface lives on the dedicated API reference page: the props (modelValue, delimiters, allowDuplicates, max, disabled / readonly, validate, placeholder, ariaLabel), the add / remove / change events, the clear() / focus() imperative handle, and the scoped tag slot.
Behaviour
| Interaction | Result |
|---|---|
| type a character | mirrors into the inline draft buffer (not yet committed). |
a delimiter key (Enter / , by default) | commits the draft as a token (after trim → validate → dedup → max), clears the draft, and fires add + change. |
| paste | the pasted text is split on the non-Enter delimiter characters and each piece is bulk-added (same validate/dedup/cap rules); fires add per accepted token. |
Backspace in an empty input | removes the previous (last) token and fires remove + change. |
| a chip's remove control (click) | removes that token and fires remove + change. |
reaching max | the input is disabled; further adds (type-commit, paste, programmatic) are rejected. |
A candidate that is empty, a duplicate (when allowDuplicates is false), rejected by validate, or over max is silently dropped — no event fires.
Theming
Every value the component renders is a --rozie-tags-* CSS custom property with a built-in fallback, so it works with zero configuration yet is completely re-skinnable. Override tokens at any ancestor scope:
css
.rozie-tags {
--rozie-tags-accent: #16a34a;
--rozie-tags-chip-bg: #dcfce7;
--rozie-tags-radius: 0.75rem;
--rozie-tags-gap: 0.5rem;
}Only cosmetic values flow through tokens; the structural rules compile per-leaf and are not consumer-overridable.
The complete token table and the design-system bridges live on the dedicated theming page.
Accessibility
- The container is a
role="group"with theariaLabelyou supply as itsaria-label; the inline text input carries the same label so assistive tech announces what is being entered. - Each chip's remove control is a real
<button>with anaria-labelof"Remove <token>", so it is reachable and announced individually. - A visually-hidden
aria-live="polite"region announces the current token count ("3 tags") as the list changes. disableddisables the input and every remove button;readonlyhides the input and remove buttons so the tokens read as a display of committed values.- Focus choreography reads a single container ref and walks
root.querySelector('input')— which reaches the input inside Lit's shadow root too — and runs only in post-mount handlers, so it is identical on all six targets.
See also
- Tags — API reference — the full prop / event / handle / slot surface.
- Headless tags input comparison — how
@rozie-ui/tagsstacks up against the per-framework token-input libraries. - Tags — live demo — the real Vue package running in the page.
Tags.roziesource on GitHub