Tags Input
React formMulti-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.
Also available for Vue ->Installation
$ pnpm dlx shadcn@latest add https://react.uipkge.dev/r/react/tags-input.json$ npx shadcn@latest add https://react.uipkge.dev/r/react/tags-input.json$ yarn dlx shadcn@latest add https://react.uipkge.dev/r/react/tags-input.json$ bunx shadcn@latest add https://react.uipkge.dev/r/react/tags-input.json
Or with the named registry:
npx shadcn@latest add @uipkge-react/tags-input
Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
value Controlled list of tags. | string[] | — | optional |
onValueChange Fires with the next list whenever a tag is added or removed. | (value: string[]) => void | — | optional |
defaultValue Uncontrolled initial list. | string[] | — | optional |
placeholder | string | — | optional |
disabled | boolean | — | optional |
addOnKeys Characters that commit the typed value into a tag. Defaults to Enter + comma. | string[] | — | optional |
unique Reject duplicate tags (case-sensitive). Defaults to true. | boolean | — | optional |
Dependencies
Files (2)
-
components/ui/tags-input/tags-input.tsx 4 kB
'use client' import * as React from 'react' import { X } from 'lucide-react' import { cn } from '@/lib/utils' export interface TagsInputProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> { /** Controlled list of tags. */ value?: string[] /** Fires with the next list whenever a tag is added or removed. */ onValueChange?: (value: string[]) => void /** Uncontrolled initial list. */ defaultValue?: string[] placeholder?: string disabled?: boolean /** Characters that commit the typed value into a tag. Defaults to Enter + comma. */ addOnKeys?: string[] /** Reject duplicate tags (case-sensitive). Defaults to true. */ unique?: boolean } const TagsInput = React.forwardRef<HTMLDivElement, TagsInputProps>( ( { className, value, onValueChange, defaultValue, placeholder, disabled, addOnKeys = ['Enter', ','], unique = true, ...props }, ref, ) => { const isControlled = value !== undefined const [internal, setInternal] = React.useState<string[]>(defaultValue ?? []) const tags = isControlled ? (value as string[]) : internal const [draft, setDraft] = React.useState('') const inputRef = React.useRef<HTMLInputElement | null>(null) function commit(next: string[]) { if (!isControlled) setInternal(next) onValueChange?.(next) } function addTag(raw: string) { const trimmed = raw.trim() if (!trimmed) return if (unique && tags.includes(trimmed)) { setDraft('') return } commit([...tags, trimmed]) setDraft('') } function removeAt(index: number) { commit(tags.filter((_, i) => i !== index)) } function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) { if (addOnKeys.includes(e.key)) { e.preventDefault() addTag(draft) } else if (e.key === 'Backspace' && draft === '' && tags.length > 0) { removeAt(tags.length - 1) } } return ( <div ref={ref} data-uipkge="" data-slot="tags-input" className={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', className, )} onClick={() => inputRef.current?.focus()} {...props} > {tags.map((tag, i) => ( <span key={`${tag}-${i}`} data-uipkge="" data-slot="tags-input-item" className="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" > <span data-slot="tags-input-item-text" className="rounded bg-transparent px-2 py-0.5 text-sm"> {tag} </span> <button type="button" aria-label={`Remove ${tag}`} disabled={disabled} data-slot="tags-input-item-delete" className="mr-1 flex rounded bg-transparent" onMouseDown={(e) => e.preventDefault()} onClick={(e) => { e.stopPropagation() removeAt(i) }} > <X className="h-4 w-4" aria-hidden="true" /> </button> </span> ))} <input ref={inputRef} data-slot="tags-input-input" value={draft} placeholder={placeholder} disabled={disabled} className="min-h-5 flex-1 bg-transparent px-1 text-sm focus:outline-none" onChange={(e) => setDraft(e.target.value)} onKeyDown={handleKeyDown} onBlur={() => addTag(draft)} /> </div> ) }, ) TagsInput.displayName = 'TagsInput' export { TagsInput } -
components/ui/tags-input/index.ts 0.1 kB
export { TagsInput, type TagsInputProps } from './tags-input'
Raw manifest: https://react.uipkge.dev/r/react/tags-input.json