Getting Started

svelte-tags-input

A fully customizable tags input component for Svelte 5.

npm version License Made with Svelte

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-input

Quick Start

Import the component and bind an array of tags.

App.svelte
<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.

PropTypeDefaultDescription
bind:tagsArray[]Two-way bound array of tags
addKeysnumber[][13] (Enter)Keys that add a tag
removeKeysnumber[][8] (Backspace)Keys that remove the last tag
placeholderstring''Input placeholder text
maxTagsnumber | falsefalseMaximum number of tags allowed
onlyUniquebooleanfalseReject duplicate tags
allowPastebooleanfalseEnable pasting tags (split by splitWith)
allowDropbooleanfalseEnable drag-and-drop of tags
allowReorderbooleanfalseReorder tags by dragging them
splitWithstring','Character to split pasted/dropped text
autoCompleteArray | (value: string) => any | falsefalseSuggestions array or async fetcher
autoCompleteKeystring | falsefalseKey for object items in autoComplete
autoCompleteShowKeystringautoCompleteKeyKey to display (when different from search key)
autoCompleteMarkupKeystring | falsefalseKey whose value is rendered as HTML in each suggestion (⚠️ not escaped — sanitize untrusted data)
autoCompleteFilterbooleantrueFilter suggestions client-side (disable for API responses)
autoCompleteStartFocusedbooleanfalseFocus first suggestion without typing
onlyAutocompletebooleanfalseOnly accept tags from the autocomplete list
minCharsnumber1Min chars before showing autocomplete (0 = show all on click)
allowBlurbooleanfalseAdd tag on input blur
cleanOnBlurbooleanfalseClear input on blur (tags kept)
disablebooleanfalseDisable the input
readonlybooleanfalseRead-only display mode
namestring'svelte-tags-input'Input name attribute
idstringrandomInput id attribute
labelTextstringnameAccessible label text
labelShowbooleanfalseShow 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) | falsefalseCustom validation before adding

Key codes

addKeys and removeKeys take numeric key codes. Common values:

KeyCode
Enter13
Tab9
Backspace8
Escape27
Comma188
Space32

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.

Reorder tags

Set allowReorder to drag tags into a new order. The bound tags only update when the tag is dropped; press Esc or drop outside to cancel.

tags = ["First","Second","Third","Fourth"]

Custom styles

Override the default styles by wrapping the component in a container with your own class.

  1. Download the CSS: svelte-tags-input-css.css
  2. Create a container for the tags with a custom class
  3. Apply your styles with :global() as shown below
App.svelte
<div class="my-custom-class">
  <Tags />
</div>
styles.css
.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.

styles.css
.my-custom-class :global(.svelte-tags-input-tag.focus),
.my-custom-class :global(.svelte-tags-input:focus) {
  background: blue;
}

Reset tags

In response to #7, reset the input after submitting a form by reassigning the bound array.

App.svelte
<script>
  import Tags from "svelte-tags-input";

  let tags = $state([]);

  function submitForm(event) {
    tags = [];
  }
</script>

<Tags bind:tags />

<button onclick={submitForm}>Submit</button>

Initial tags

Start the input with preloaded tags.

App.svelte
<script>
  import Tags from "svelte-tags-input";

  let tags = $state(["start", "with", "this", "tags"]);
</script>

<Tags bind:tags />

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:

App.svelte
<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
<!-- 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:

index.html
<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>.

Made by Agustín from Argentina

© 2026 · MIT License