@rozie-ui/tags-svelte
v0.1.3
Published
Idiomatic Svelte headless WAI-ARIA tags / token input (chips with removable controls, type-to-add with configurable delimiters, paste-to-bulk-add, dedup, validation, max cap) — one accessible Rozie source compiled to Svelte.
Maintainers
Readme
@rozie-ui/tags-svelte
Idiomatic svelte Tags — a headless, fully-accessible (WAI-ARIA) tags / token input (removable chips, type-to-add with configurable delimiters, paste-to-bulk-add, dedup, per-token validation, a max cap, and a scoped #tag slot for custom chip rendering) compiled from one Rozie source. The interaction engine IS the browser's native <input> plus the platform clipboard/keyboard; every visual value is a CSS custom property, so it re-skins to any design system. This package is generated; do not edit src/ by hand.
Install
npm i @rozie-ui/tags-sveltePeer dependencies: svelte. Install them alongside this package.
Also installed: @rozie/runtime-svelte — Rozie's small, tree-shaken runtime helper package (controllable state, keyboard navigation, event modifiers, and safe interpolation). It arrives as a regular dependency, so npm pulls it for you. Your bundler keeps only the helpers this component actually uses — typically a few hundred bytes to a few KB, minified and gzipped. What's in it and what it costs.
Usage
<script lang="ts">
import Tags from '@rozie-ui/tags-svelte';
let skills = $state<string[]>(['rozie', 'svelte']);
</script>
<Tags
bind:modelValue={skills}
placeholder="Add a skill…"
ariaLabel="Skills"
max={8}
onadd={(e) => console.log('added', e.value)}
/>
<!-- Custom chip via the scoped #tag snippet -->
<Tags bind:modelValue={skills} ariaLabel="Skills">
{#snippet tag({ tag, remove })}
<span class="pill">{tag} <button type="button" onclick={remove}>×</button></span>
{/snippet}
</Tags>Theming
Every visual value is a --rozie-tags-* CSS custom property — override any of them at any ancestor scope. Ready-made design-system bridges ship in the package:
import '@rozie-ui/tags-svelte/themes/shadcn.css'; // or material.css, bootstrap.css, base.cssProps
| Name | Type | Default | Two-way (model) | Required | Description |
| --- | --- | --- | :---: | :---: | --- |
| modelValue | Array | [] | ✓ | | The committed tokens — model: true, so a commit/remove/paste writes a fresh array back through r-model:modelValue (uncontrolled fallback []). Because it is the sole model prop, the Angular output is a ControlValueAccessor ([formControl] / [(ngModel)] bind directly). |
| delimiters | Array | […] | | | The keys that commit the current draft as a token (matched against the key event's key). Default [',', 'Enter']. Non-'Enter' entries also act as the split characters when pasting bulk text. Use e.g. [' ', 'Enter'] for a space-delimited input. |
| allowDuplicates | Boolean | false | | | Allow the same token value to be added more than once. Defaults to false — a candidate equal (case-sensitive) to an existing token is silently rejected on commit. Set true to permit duplicates. |
| max | Number | null | | | Maximum number of tokens. Once the list reaches max, the input is disabled and further adds (type, paste, programmatic) are rejected. null (the default) means unlimited. |
| disabled | Boolean | false | | | Disable the whole control — the text input is disabled, every remove button is disabled, and no token can be added or removed. Also sets the Angular CVA disabled state. |
| readonly | Boolean | false | | | Render the tokens read-only — they remain visible but cannot be added or removed, and the text input is hidden. Unlike disabled it carries no disabled styling, so it reads as a display of committed values. |
| validate | Function | null | | | Optional per-token validator / normalizer. Called with (candidate, tokens) for each commit; return a (possibly normalized) string to accept it, or a falsy value (false / null / "") to reject the candidate. Runs before the dedup + max checks. Example: v => /^\S+@\S+$/.test(v) ? v.toLowerCase() : false for emails. |
| placeholder | String | '' | | | Placeholder text for the inline text input (e.g. "Add a tag…"). |
| ariaLabel | String | null | | | Accessible name for the whole control (role="group"). The inline text input is labelled with the same name so assistive tech announces what is being entered. A visually-hidden live region announces the current token count on change. |
Events
| Event | Description |
| --- | --- |
| change | Fired on every committed-list mutation (add, remove, paste-bulk-add, or a programmatic clear). Payload { value } — the new full tokens array. Use it to observe the list without two-way binding. |
| add | Fired when a token is committed (an accepted Enter/comma/paste add). Payload { value, tokens } — value is the newly added token string, tokens the fresh full array. Rejected candidates (duplicate, failed validate, over max) do NOT fire it. |
| remove | Fired when a token is removed (a chip remove-button click or Backspace in an empty input). Payload { value, index, tokens } — the removed token, its former index, and the fresh full array. |
Imperative handle
Beyond props, the component exposes imperative methods (declared once in the Rozie source via $expose). Grab a handle with the native ref mechanism and call them directly:
| Method | Description |
| --- | --- |
| clear | Remove every token (emits change with { value: [] }) and move DOM focus to the text input. Collision-safe — not a host-element member. |
| focus | Move DOM focus to the inline text input. NOTE: this deliberately overrides the inherited HTMLElement.focus on the Lit custom element (ROZ137 warns, warn-only) — the public focus() handle is the intended semantics. |
<script>
let tags; // component instance via bind:this
</script>
<Tags bind:this={tags} bind:modelValue={skills} />
<button onclick={() => tags.clear()}>Clear</button>Slots
| Slot | Params | | --- | --- | | tag | tag, index, remove |
The scoped tag slot lets you fully replace each chip; its params are { tag, index, remove } (the token string, its index, and a zero-arg remove() for that token). On React the slot is a render-prop children callback (the documented cross-framework slot divergence).
