Tags Input
tags-inputuiMulti-tag input — type a value, hit Enter, get a Chip. Backspace removes the last tag. Use for email recipient lists, tag sets, and free-form keyword inputs.
Installation
$pnpm dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/tags-input.json$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/tags-input.json$yarn dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/tags-input.json$bunx shadcn-svelte@latest add https://uipkge.dev/r/svelte/tags-input.jsonhttps://uipkge.dev/r/svelte/tags-input.jsonInstalls to:src/lib/components/ui/tags-input/Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
valueControlled tag list. Bind with `bind:value`. | string[] | — | optional |
defaultValueInitial tags for uncontrolled usage. | string[] | — | optional |
disabled | boolean | false | optional |
placeholderPlaceholder for the inner input (React parity; the input prop still wins when set). | string | — | optional |
addOnPasteSplit pasted text on whitespace and add each token. | boolean | false | optional |
addOnBlurAlso commit the input when it blurs. | boolean | false | optional |
addOnKeysKeys that commit the typed value into a tag. Defaults to Enter + comma (React parity). | string[] | — | optional |
delimiter@deprecated Use `addOnKeys`. Kept as an alias — both stay functional. | string | — | optional |
uniqueReject duplicate tags (case-sensitive). Defaults to true (React parity). | boolean | true | optional |
maxMaximum number of tags. | number | — | optional |
ref | HTMLDivElement | null | — | optional |
onValueChange | (value: string[]) => void | — | optional |
Schema
Type aliases from this item's source — use them to shape the data you pass in.
TagsInputContextValueinterface TagsInputContextValue {
values: () => string[]
disabled: () => boolean
addValue: (raw: string) => void
removeValue: (value: string) => void
removeLast: () => void
addOnPaste: () => boolean
addOnBlur: () => boolean
delimiter: () => string | undefined
/** Resolved commit keys (React `addOnKeys` parity; `delimiter` folds in as an alias). */
commitKeys: () => string[]
/** Root-level placeholder (React parity); the input falls back to it. */
placeholder: () => string | undefined
}npm dependencies
Files installed (7)
src/lib/components/ui/tags-input/TagsInput.svelte3.3 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface TagsInputProps extends HTMLAttributes<HTMLDivElement> { /** Controlled tag list. Bind with `bind:value`. */ value?: string[] /** Initial tags for uncontrolled usage. */ defaultValue?: string[] disabled?: boolean /** Placeholder for the inner input (React parity; the input prop still wins when set). */ placeholder?: string /** Split pasted text on whitespace and add each token. */ addOnPaste?: boolean /** Also commit the input when it blurs. */ addOnBlur?: boolean /** Keys that commit the typed value into a tag. Defaults to Enter + comma (React parity). */ addOnKeys?: string[] /** Commit the input when this character is typed (in addition to Enter). * @deprecated Use `addOnKeys`. Kept as an alias — both stay functional. */ delimiter?: string /** Reject duplicate tags (case-sensitive). Defaults to true (React parity). */ unique?: boolean /** Maximum number of tags. */ max?: number ref?: HTMLDivElement | null onValueChange?: (value: string[]) => void } </script> <script lang="ts"> import { cn } from '$lib/utils' import { setTagsInputContext } from './context' let { class: className, defaultValue = [], value = $bindable(defaultValue), disabled = false, placeholder, addOnPaste = false, addOnBlur = false, addOnKeys, delimiter, unique = true, max, children, ref = $bindable(null), onValueChange, ...restProps }: TagsInputProps = $props() const commitKeys = $derived.by((): string[] => { if (addOnKeys) return addOnKeys if (delimiter) return [...new Set(['Enter', delimiter])] return ['Enter', ','] }) function commit(next: string[]) { value = next onValueChange?.(next) } function addValue(raw: string) { if (disabled) return const trimmed = raw.trim() if (!trimmed) return const current = value ?? [] if (unique && current.includes(trimmed)) return if (max !== undefined && current.length >= max) return commit([...current, trimmed]) } function removeValue(target: string) { if (disabled) return commit((value ?? []).filter((v) => v !== target)) } function removeLast() { if (disabled) return const current = value ?? [] if (current.length === 0) return commit(current.slice(0, -1)) } setTagsInputContext({ values: () => value ?? [], disabled: () => disabled, addValue, removeValue, removeLast, addOnPaste: () => addOnPaste, addOnBlur: () => addOnBlur, delimiter: () => delimiter, commitKeys: () => commitKeys, placeholder: () => placeholder, }) </script> <div bind:this={ref} data-uipkge="" data-slot="tags-input" data-disabled={disabled || undefined} class={cn( 'border-input bg-background flex flex-wrap items-center gap-2 rounded-md border px-2 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none', 'focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]', 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive', disabled && 'cursor-not-allowed opacity-60', className, )} {...restProps} > {@render children?.()} </div>src/lib/components/ui/tags-input/TagsInputInput.svelte2.4 kB
<script lang="ts" module> import type { HTMLInputAttributes } from 'svelte/elements' export interface TagsInputInputProps extends HTMLInputAttributes { ref?: HTMLInputElement | null } </script> <script lang="ts"> import type { ClipboardEventHandler, FocusEventHandler, KeyboardEventHandler } from 'svelte/elements' import { cn } from '$lib/utils' import { getTagsInputContext } from './context' let { class: className, disabled, placeholder, ref = $bindable(null), onkeydown, onpaste, onblur, ...restProps }: TagsInputInputProps = $props() const ctx = getTagsInputContext() const isDisabled = $derived(disabled ?? ctx?.disabled() ?? false) const effectivePlaceholder = $derived(placeholder ?? ctx?.placeholder()) const handleKeydown: KeyboardEventHandler<HTMLInputElement> = (event) => { onkeydown?.(event) if (event.defaultPrevented || isDisabled) return const input = event.currentTarget const commitKeys = ctx?.commitKeys() ?? ['Enter', ','] if (commitKeys.includes(event.key)) { event.preventDefault() if (input.value) { ctx?.addValue(input.value) input.value = '' } } else if (event.key === 'Backspace' && input.value === '') { event.preventDefault() ctx?.removeLast() } } const handlePaste: ClipboardEventHandler<HTMLInputElement> = (event) => { onpaste?.(event) if (event.defaultPrevented || isDisabled || !ctx?.addOnPaste()) return const text = event.clipboardData?.getData('text') if (!text) return event.preventDefault() const delimiter = ctx.delimiter() const tokens = delimiter ? text.split(delimiter) : text.split(/\s+/) for (const token of tokens) ctx.addValue(token) event.currentTarget.value = '' } const handleBlur: FocusEventHandler<HTMLInputElement> = (event) => { onblur?.(event) if (event.defaultPrevented || isDisabled || !ctx?.addOnBlur()) return if (event.currentTarget.value) { ctx.addValue(event.currentTarget.value) event.currentTarget.value = '' } } </script> <input bind:this={ref} data-uipkge="" data-slot="tags-input-input" disabled={isDisabled} aria-label="Add a tag" placeholder={effectivePlaceholder} class={cn('min-h-5 flex-1 bg-transparent px-1 text-sm focus:outline-none', className)} onkeydown={handleKeydown} onpaste={handlePaste} onblur={handleBlur} {...restProps} />src/lib/components/ui/tags-input/TagsInputItem.svelte0.8 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface TagsInputItemProps extends HTMLAttributes<HTMLDivElement> { value: string ref?: HTMLDivElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' import { setTagsInputItem } from './context' let { class: className, value, children, ref = $bindable(null), ...restProps }: TagsInputItemProps = $props() setTagsInputItem(() => value) </script> <div bind:this={ref} data-uipkge="" data-slot="tags-input-item" data-value={value} class={cn( 'bg-secondary data-[state=active]:ring-ring ring-offset-background flex h-5 items-center rounded-md data-[state=active]:ring-2 data-[state=active]:ring-offset-2', className, )} {...restProps} > {@render children?.()} </div>src/lib/components/ui/tags-input/TagsInputItemDelete.svelte1.3 kB
<script lang="ts" module> import type { HTMLButtonAttributes } from 'svelte/elements' export interface TagsInputItemDeleteProps extends HTMLButtonAttributes { ref?: HTMLButtonElement | null } </script> <script lang="ts"> import { X } from '@lucide/svelte' import { cn } from '$lib/utils' import { getTagsInputContext, getTagsInputItem } from './context' let { class: className, children, ref = $bindable(null), disabled, onclick, ...restProps }: TagsInputItemDeleteProps = $props() const ctx = getTagsInputContext() const getItem = getTagsInputItem() const itemValue = $derived(getItem?.() ?? null) const isDisabled = $derived(disabled ?? ctx?.disabled() ?? false) </script> <button bind:this={ref} type="button" data-uipkge="" data-slot="tags-input-item-delete" aria-label={itemValue ? `Remove ${itemValue}` : 'Remove tag'} disabled={isDisabled} class={cn( 'hover:text-foreground focus-visible:ring-ring mr-1 flex rounded bg-transparent focus-visible:ring-2 focus-visible:outline-none', className, )} onclick={(event) => { onclick?.(event) if (!event.defaultPrevented && itemValue) ctx?.removeValue(itemValue) }} {...restProps} > {#if children}{@render children()}{:else}<X class="h-4 w-4" aria-hidden="true" />{/if} </button>src/lib/components/ui/tags-input/TagsInputItemText.svelte0.9 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface TagsInputItemTextProps extends HTMLAttributes<HTMLSpanElement> { ref?: HTMLSpanElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' import { getTagsInputItem } from './context' let { class: className, children, ref = $bindable(null), ...restProps }: TagsInputItemTextProps = $props() // Renders the enclosing item's value when no explicit children are given // (mirrors reka-ui TagsInputItemText default content). const getItem = getTagsInputItem() const itemValue = $derived(getItem?.() ?? null) </script> <span bind:this={ref} data-uipkge="" data-slot="tags-input-item-text" class={cn('rounded bg-transparent px-2 py-0.5 text-sm', className)} {...restProps} >{#if children}{@render children()}{:else}{itemValue}{/if}</span >src/lib/components/ui/tags-input/context.ts1.3 kB
import { getContext, setContext } from 'svelte' /** * Tags-input state shared from <TagsInput> to its descendants. Exposed as * functions so reads inside child templates/`$derived` track the root's * runes state. */ export interface TagsInputContextValue { values: () => string[] disabled: () => boolean addValue: (raw: string) => void removeValue: (value: string) => void removeLast: () => void addOnPaste: () => boolean addOnBlur: () => boolean delimiter: () => string | undefined /** Resolved commit keys (React `addOnKeys` parity; `delimiter` folds in as an alias). */ commitKeys: () => string[] /** Root-level placeholder (React parity); the input falls back to it. */ placeholder: () => string | undefined } const KEY = Symbol.for('uipkge:tags-input') export function setTagsInputContext(value: TagsInputContextValue): TagsInputContextValue { setContext(KEY, value) return value } export function getTagsInputContext(): TagsInputContextValue | null { return getContext<TagsInputContextValue | null>(KEY) ?? null } const ITEM_KEY = Symbol.for('uipkge:tags-input-item') export function setTagsInputItem(getter: () => string): void { setContext(ITEM_KEY, getter) } export function getTagsInputItem(): (() => string) | null { return getContext<(() => string) | null>(ITEM_KEY) ?? null }src/lib/components/ui/tags-input/index.ts0.5 kB
export { default as TagsInput, type TagsInputProps } from './TagsInput.svelte' export { default as TagsInputInput, type TagsInputInputProps } from './TagsInputInput.svelte' export { default as TagsInputItem, type TagsInputItemProps } from './TagsInputItem.svelte' export { default as TagsInputItemDelete, type TagsInputItemDeleteProps } from './TagsInputItemDelete.svelte' export { default as TagsInputItemText, type TagsInputItemTextProps } from './TagsInputItemText.svelte'
Raw manifest:https://uipkge.dev/r/svelte/tags-input.json