Getting Started
svelte-tags-input
A fully customizable tags input component for Svelte 5.
Keyboard-driven
Add tags with Enter/Tab, remove with Backspace, navigate autocomplete with arrow keys.
Autocomplete
Array or async function, with filtering and custom display keys.
Paste, drop & reorder
Paste comma-separated text, drag-and-drop tags, or drag tags to reorder them.
Accessible
ARIA-friendly, screen reader labels and keyboard navigation.
Flexible
Max tags, unique-only, custom validation and blur behavior.
Svelte 5
Built with runes: $props, $state, $derived and $bindable.
npm install svelte-tags-inputQuick Start
Import the component and bind an array of tags.
<script>
import Tags from "svelte-tags-input";
let tags = $state([]);
</script>
<Tags bind:tags placeholder="Type and press Enter..." />Options
All props accepted by the <Tags /> component.
| Prop | Type | Default | Description |
|---|---|---|---|
| bind:tags | Array | [] | Two-way bound array of tags |
| addKeys | number[] | [13] (Enter) | Keys that add a tag |
| removeKeys | number[] | [8] (Backspace) | Keys that remove the last tag |
| placeholder | string | '' | Input placeholder text |
| maxTags | number | false | false | Maximum number of tags allowed |
| onlyUnique | boolean | false | Reject duplicate tags |
| allowPaste | boolean | false | Enable pasting tags (split by splitWith) |
| allowDrop | boolean | false | Enable drag-and-drop of tags |
| allowReorder | boolean | false | Reorder tags by dragging them |
| splitWith | string | ',' | Character to split pasted/dropped text |
| autoComplete | Array | (value: string) => any | false | false | Suggestions array or async fetcher |
| autoCompleteKey | string | false | false | Key for object items in autoComplete |
| autoCompleteShowKey | string | autoCompleteKey | Key to display (when different from search key) |
| autoCompleteMarkupKey | string | false | false | Key whose value is rendered as HTML in each suggestion (⚠️ not escaped — sanitize untrusted data) |
| autoCompleteFilter | boolean | true | Filter suggestions client-side (disable for API responses) |
| autoCompleteStartFocused | boolean | false | Focus first suggestion without typing |
| onlyAutocomplete | boolean | false | Only accept tags from the autocomplete list |
| minChars | number | 1 | Min chars before showing autocomplete (0 = show all on click) |
| allowBlur | boolean | false | Add tag on input blur |
| cleanOnBlur | boolean | false | Clear input on blur (tags kept) |
| disable | boolean | false | Disable the input |
| readonly | boolean | false | Read-only display mode |
| name | string | 'svelte-tags-input' | Input name attribute |
| id | string | random | Input id attribute |
| labelText | string | name | Accessible label text |
| labelShow | boolean | false | Show visible label |
| onTagAdded | (tag, tags) => void | () => {} | Called when a tag is added |
| onTagRemoved | (tag, tags) => void | () => {} | Called when a tag is removed |
| onTagClick | (tag) => void | () => {} | Called when a tag is clicked |
| onTagReordered | (tag, tags, fromIndex, toIndex) => void | () => {} | Called when a tag is dropped at a new position |
| customValidation | ((tag) => boolean) | false | false | Custom validation before adding |
Key codes
addKeys and removeKeys take numeric key codes. Common values:
| Key | Code |
|---|---|
| Enter | 13 |
| Tab | 9 |
| Backspace | 8 |
| Escape | 27 |
| Comma | 188 |
| Space | 32 |
See the MDN keyCode reference for others.
Full example
Every option at once: Tab to add, Esc to remove, up to 5 unique countries from the autocomplete list.
tags = []
Async autocomplete
Pass an async function to autoComplete to fetch suggestions from an API. Objects work too, using autoCompleteKey to pick the label.
Custom styles
Override the default styles by wrapping the component in a container with your own class.
- Download the CSS: svelte-tags-input-css.css
- Create a container for the tags with a custom class
- Apply your styles with
:global()as shown below
<div class="my-custom-class">
<Tags />
</div>.my-custom-class :global(.svelte-tags-input-tag) {
background: blue;
}
.my-custom-class :global(.svelte-tags-input-layout) {
background: yellow;
}A .focus class is available in case you want to override the input focus style.
.my-custom-class :global(.svelte-tags-input-tag.focus),
.my-custom-class :global(.svelte-tags-input:focus) {
background: blue;
}Inside a form
The name prop is applied to the visible text input, so a native form submit only sends whatever is
currently typed — not the tags. To submit the tags, render a hidden input for each one:
<script>
import Tags from 'svelte-tags-input';
let tags = $state([]);
</script>
<form method="POST" action="/save">
<Tags bind:tags name="tags-text" placeholder="Add tags..." />
{#each tags as tag}
<input type="hidden" name="tags" value={tag} />
{/each}
<button type="submit">Save</button>
</form>On the server, read them with formData.getAll('tags'). Any text still typed in the input arrives
separately as tags-text.
If your tags are objects (from an autoComplete list of objects), serialize them instead:
<input type="hidden" name="tags" value={JSON.stringify(tags)} />As a custom element
Use it from Lit, vanilla JS or other frameworks. The package doesn't ship a custom element build, but you can wrap it in your own Svelte component and compile that as a custom element:
<!-- TagsInputElement.svelte -->
<svelte:options
customElement={{
tag: 'tags-input',
props: {
tags: { type: 'Array', reflect: false },
placeholder: { type: 'String' },
autoComplete: { type: 'Array', attribute: 'auto-complete' }
}
}}
/>
<script>
import Tags from 'svelte-tags-input';
let { tags = [], placeholder = '', autoComplete = false } = $props();
const host = $host();
const emit = (type, detail) =>
host.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true }));
</script>
<Tags
bind:tags
{placeholder}
{autoComplete}
onTagAdded={(tag, all) => emit('tag-added', { tag, tags: all })}
onTagRemoved={(tag, all) => emit('tag-removed', { tag, tags: all })}
/>Compile it with the Svelte compiler options customElement: true and css: 'injected' (so the component styles are included in the shadow DOM), then use it anywhere:
<tags-input placeholder="Add tags..."></tags-input>
<script type="module">
const el = document.querySelector('tags-input');
el.addEventListener('tag-added', (e) => console.log(e.detail.tag, e.detail.tags));
</script>Expose any other props you need the same way, adding them to props and passing them through to <Tags>.