UIPackage

Tags Input

React form
Edit on GitHub

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

$ npx 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