Components
Tags Input
Display a list of tags in an input field with the ability to add, edit, and remove them.
"use client";
 
import * as TagsInput from "@diceui/tags-input";
import { RefreshCcw, X } from "lucide-react";
import * as React from "react";
 
export function TagsInputDemo() {
  const [tricks, setTricks] = React.useState<string[]>([]);
 
  return (
    <TagsInput.Root
      value={tricks}
      onValueChange={setTricks}
      className="flex w-[380px] flex-col gap-2"
      editable
    >
      <TagsInput.Label className="font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
        Tricks
      </TagsInput.Label>
      <div className="flex min-h-10 w-full flex-wrap items-center gap-1.5 rounded-md border border-input bg-background px-3 py-2 text-sm focus-within:ring-1 focus-within:ring-zinc-500 disabled:cursor-not-allowed disabled:opacity-50 dark:focus-within:ring-zinc-400">
        {tricks.map((trick) => (
          <TagsInput.Item
            key={trick}
            value={trick}
            className="inline-flex max-w-[calc(100%-8px)] items-center gap-1.5 rounded border bg-transparent px-2.5 py-1 text-sm focus:outline-hidden data-disabled:cursor-not-allowed data-editable:select-none data-editing:bg-transparent data-disabled:opacity-50 data-editing:ring-1 data-editing:ring-zinc-500 dark:data-editing:ring-zinc-400 [&:not([data-editing])]:pr-1.5 [&[data-highlighted]:not([data-editing])]:bg-zinc-200 [&[data-highlighted]:not([data-editing])]:text-black dark:[&[data-highlighted]:not([data-editing])]:bg-zinc-800 dark:[&[data-highlighted]:not([data-editing])]:text-white"
          >
            <TagsInput.ItemText className="truncate" />
            <TagsInput.ItemDelete className="h-4 w-4 shrink-0 rounded-sm opacity-70 ring-offset-zinc-950 transition-opacity hover:opacity-100">
              <X className="h-3.5 w-3.5" />
            </TagsInput.ItemDelete>
          </TagsInput.Item>
        ))}
        <TagsInput.Input
          placeholder="Add trick..."
          className="flex-1 bg-transparent outline-hidden placeholder:text-zinc-500 disabled:cursor-not-allowed disabled:opacity-50 dark:placeholder:text-zinc-400"
        />
      </div>
      <TagsInput.Clear className="flex h-9 items-center justify-center gap-2 rounded-sm border border-input bg-transparent text-zinc-800 shadow-xs hover:bg-zinc-100/80 dark:text-zinc-300 dark:hover:bg-zinc-900/80">
        <RefreshCcw className="h-4 w-4" />
        Clear
      </TagsInput.Clear>
    </TagsInput.Root>
  );
}Installation
npm install @diceui/tags-inputInstallation with shadcn/ui
CLI
npx shadcn@latest add "https://diceui.com/r/tags-input"Manual
Install the following dependencies:
npm install @diceui/tags-inputCopy and paste the following code into your project.
import * as TagsInputPrimitive from "@diceui/tags-input";
import { X } from "lucide-react";
import type * as React from "react";
import { cn } from "@/lib/utils";
 
function TagsInput({
  className,
  ...props
}: React.ComponentProps<typeof TagsInputPrimitive.Root>) {
  return (
    <TagsInputPrimitive.Root
      data-slot="tags-input"
      className={cn("flex w-[380px] flex-col gap-2", className)}
      {...props}
    />
  );
}
 
function TagsInputLabel({
  className,
  ...props
}: React.ComponentProps<typeof TagsInputPrimitive.Label>) {
  return (
    <TagsInputPrimitive.Label
      data-slot="tags-input-label"
      className={cn(
        "font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
        className,
      )}
      {...props}
    />
  );
}
 
function TagsInputList({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="tags-input-list"
      className={cn(
        "flex min-h-10 w-full flex-wrap items-center gap-1.5 rounded-md border border-input bg-background px-3 py-2 text-sm focus-within:ring-1 focus-within:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
        className,
      )}
      {...props}
    />
  );
}
 
function TagsInputInput({
  className,
  ...props
}: React.ComponentProps<typeof TagsInputPrimitive.Input>) {
  return (
    <TagsInputPrimitive.Input
      data-slot="tags-input-input"
      className={cn(
        "flex-1 bg-transparent outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
        className,
      )}
      {...props}
    />
  );
}
 
function TagsInputItem({
  className,
  children,
  ...props
}: React.ComponentProps<typeof TagsInputPrimitive.Item>) {
  return (
    <TagsInputPrimitive.Item
      data-slot="tags-input-item"
      className={cn(
        "inline-flex max-w-[calc(100%-8px)] items-center gap-1.5 rounded border bg-transparent px-2.5 py-1 text-sm focus:outline-hidden data-disabled:cursor-not-allowed data-editable:select-none data-editing:bg-transparent data-disabled:opacity-50 data-editing:ring-1 data-editing:ring-ring [&:not([data-editing])]:pr-1.5 [&[data-highlighted]:not([data-editing])]:bg-accent [&[data-highlighted]:not([data-editing])]:text-accent-foreground",
        className,
      )}
      {...props}
    >
      <TagsInputPrimitive.ItemText className="truncate">
        {children}
      </TagsInputPrimitive.ItemText>
      <TagsInputPrimitive.ItemDelete className="size-4 shrink-0 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100">
        <X className="size-3.5" />
      </TagsInputPrimitive.ItemDelete>
    </TagsInputPrimitive.Item>
  );
}
 
function TagsInputClear({
  ...props
}: React.ComponentProps<typeof TagsInputPrimitive.Clear>) {
  return <TagsInputPrimitive.Clear data-slot="tags-input-clear" {...props} />;
}
 
export {
  TagsInput,
  TagsInputLabel,
  TagsInputList,
  TagsInputInput,
  TagsInputItem,
  TagsInputClear,
};Layout
Import the parts, and compose them together.
import * as TagsInput from "@diceui/tags-input";
<TagsInput.Root>
  <TagsInput.Label/>
  <TagsInput.Item >
    <TagsInput.ItemText />
    <TagsInput.ItemDelete />
  </TagsInput.Item>
  <TagsInput.Input />
  <TagsInput.Clear />
</TagsInput.Root>Examples
Editable
"use client";
 
import { RefreshCcw } from "lucide-react";
import * as React from "react";
import { Button } from "@/components/ui/button";
import {
  TagsInput,
  TagsInputClear,
  TagsInputInput,
  TagsInputItem,
  TagsInputLabel,
  TagsInputList,
} from "@/components/ui/tags-input";
 
export function TagsInputEditableDemo() {
  const [tricks, setTricks] = React.useState([
    "Kickflip",
    "Heelflip",
    "FS 540",
  ]);
 
  return (
    <TagsInput value={tricks} onValueChange={setTricks} editable addOnPaste>
      <TagsInputLabel>Tricks</TagsInputLabel>
      <TagsInputList>
        {tricks.map((trick) => (
          <TagsInputItem key={trick} value={trick}>
            {trick}
          </TagsInputItem>
        ))}
        <TagsInputInput placeholder="Add trick..." />
      </TagsInputList>
      <TagsInputClear asChild>
        <Button variant="outline">
          <RefreshCcw className="h-4 w-4" />
          Clear
        </Button>
      </TagsInputClear>
    </TagsInput>
  );
}With Validation
Validate the input value before adding it to the list. Can be used to prevent duplicate tags.
"use client";
 
import * as React from "react";
import { toast } from "sonner";
import {
  TagsInput,
  TagsInputInput,
  TagsInputItem,
  TagsInputLabel,
  TagsInputList,
} from "@/components/ui/tags-input";
 
export function TagsInputValidationDemo() {
  const [tricks, setTricks] = React.useState<string[]>([]);
 
  return (
    <TagsInput
      value={tricks}
      onValueChange={setTricks}
      onValidate={(value) => value.length > 2 && !value.includes("ollie")}
      onInvalid={(value) =>
        tricks.length >= 6
          ? toast.error("Up to 6 tricks are allowed.")
          : tricks.includes(value)
            ? toast.error(`${value} already exists.`)
            : toast.error(`${value} is not a valid trick.`)
      }
      max={6}
      editable
      addOnPaste
    >
      <TagsInputLabel>Tricks</TagsInputLabel>
      <TagsInputList>
        {tricks.map((trick) => (
          <TagsInputItem key={trick} value={trick}>
            {trick}
          </TagsInputItem>
        ))}
        <TagsInputInput placeholder="Add trick..." />
      </TagsInputList>
      <div className="text-muted-foreground text-sm">
        Add up to 6 tricks with at least 3 characters, excluding "ollie".
      </div>
    </TagsInput>
  );
}With Sortable
TagsInput can be composed with Sortable to allow reordering of tags.
"use client";
 
import { MouseSensor, TouchSensor, useSensor, useSensors } from "@dnd-kit/core";
import * as React from "react";
 
import {
  Sortable,
  SortableContent,
  SortableItem,
  SortableOverlay,
} from "@/components/ui/sortable";
import {
  TagsInput,
  TagsInputInput,
  TagsInputItem,
  TagsInputLabel,
  TagsInputList,
} from "@/components/ui/tags-input";
 
export function TagsInputSortableDemo() {
  const [tricks, setTricks] = React.useState(["The 900", "FS 540"]);
 
  const sensors = useSensors(
    useSensor(MouseSensor, {
      activationConstraint: { distance: 8 },
    }),
    useSensor(TouchSensor, {
      activationConstraint: { delay: 250, tolerance: 5 },
    }),
  );
 
  return (
    <Sortable
      sensors={sensors}
      value={tricks}
      onValueChange={(items) => setTricks(items)}
      orientation="mixed"
      flatCursor
    >
      <TagsInput value={tricks} onValueChange={setTricks} editable>
        <TagsInputLabel>Sortable</TagsInputLabel>
        <SortableContent>
          <TagsInputList>
            {tricks.map((trick) => (
              <SortableItem
                key={trick}
                value={trick}
                // to prevent tag item from being tabbable
                tabIndex={-1}
                asChild
                asHandle
              >
                <TagsInputItem value={trick}>{trick}</TagsInputItem>
              </SortableItem>
            ))}
            <TagsInputInput placeholder="Add trick..." />
          </TagsInputList>
        </SortableContent>
        <SortableOverlay>
          <div className="size-full animate-pulse rounded-sm bg-primary/10" />
        </SortableOverlay>
      </TagsInput>
    </Sortable>
  );
}API Reference
Root
Container for the tags input.
Prop
Type
| Data Attribute | Value | 
|---|---|
| [data-disabled] | Present when disabled. | 
| [data-invalid] | Present when invalid. | 
| [data-readonly] | Present when readOnly. | 
Label
Label element for the tags input.
Prop
Type
| Data Attribute | Value | 
|---|---|
| [data-disabled] | Present when disabled. | 
Input
Text input for adding new tags.
Prop
Type
| Data Attribute | Value | 
|---|---|
| [data-invalid] | Present when invalid. | 
Item
Individual tag item.
Prop
Type
| Data Attribute | Value | 
|---|---|
| [data-state] | "active" | "inactive" | 
| [data-highlighted] | Present when highlighted. | 
| [data-disabled] | Present when disabled. | 
| [data-editing] | Present when being edited. | 
| [data-editable] | Present when the tags input is editable. | 
ItemText
Text content of a tag.
Prop
Type
ItemDelete
Button to remove a tag.
Prop
Type
| Data Attribute | Value | 
|---|---|
| [data-disabled] | Present when disabled. | 
| [data-state] | "active" | "inactive" | 
Clear
Button to clear all tags.
Prop
Type
| Data Attribute | Value | 
|---|---|
| [data-disabled] | Present when disabled. | 
| [data-state] | "visible" | "invisible" | 
Accessibility
Keyboard Interactions
| Key | Description | 
|---|---|
| Delete | When a tag is highlighted, removes it and sets focus to the next tag. | 
| Backspace | When a tag is highlighted, removes it and sets focus to the previous tag. If there is no previous tag, focus moves to the next tag or input. | 
| ArrowLeft | Highlights the previous tag. When cursor is at start of input, moves focus to the last tag. | 
| ArrowRight | Highlights the next tag. When cursor is at start of input, moves focus to the first tag. | 
| Home | Highlights the first tag in the list. | 
| End | Highlights the last tag in the list. | 
| Enter | When input has text, adds a new tag. When a tag is highlighted and editable is enabled, enters edit mode. | 
| Escape | Clears tag highlight and edit mode, resets cursor to start. | 
| Tab | When `addOnTab` is enabled and input has text, adds a new tag. Otherwise, blurs the input. |