Dice UI
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-input
pnpm add @diceui/tags-input
yarn add @diceui/tags-input
bun add @diceui/tags-input

Installation with shadcn/ui

CLI

npx shadcn@latest add "https://diceui.com/r/tags-input"
pnpm dlx shadcn@latest add "https://diceui.com/r/tags-input"
yarn dlx shadcn@latest add "https://diceui.com/r/tags-input"
bun x shadcn@latest add "https://diceui.com/r/tags-input"

Manual

Install the following dependencies:

npm install @diceui/tags-input
pnpm add @diceui/tags-input
yarn add @diceui/tags-input
bun add @diceui/tags-input

Copy and paste the following code into your project.

import { cn } from "@/lib/utils";
import * as TagsInputPrimitive from "@diceui/tags-input";
import { X } from "lucide-react";
import * as React from "react";
 
const TagsInput = React.forwardRef<
  React.ComponentRef<typeof TagsInputPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof TagsInputPrimitive.Root>
>(({ className, ...props }, ref) => (
  <TagsInputPrimitive.Root
    data-slot="tags-input"
    ref={ref}
    className={cn("flex w-[380px] flex-col gap-2", className)}
    {...props}
  />
));
TagsInput.displayName = TagsInputPrimitive.Root.displayName;
 
const TagsInputLabel = React.forwardRef<
  React.ComponentRef<typeof TagsInputPrimitive.Label>,
  React.ComponentPropsWithoutRef<typeof TagsInputPrimitive.Label>
>(({ className, ...props }, ref) => (
  <TagsInputPrimitive.Label
    data-slot="tags-input-label"
    ref={ref}
    className={cn(
      "font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
      className,
    )}
    {...props}
  />
));
TagsInputLabel.displayName = TagsInputPrimitive.Label.displayName;
 
const TagsInputList = React.forwardRef<
  HTMLDivElement,
  React.ComponentPropsWithoutRef<"div">
>(({ className, ...props }, ref) => (
  <div
    data-slot="tags-input-list"
    ref={ref}
    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}
  />
));
TagsInputList.displayName = "TagsInputList";
 
const TagsInputInput = React.forwardRef<
  React.ComponentRef<typeof TagsInputPrimitive.Input>,
  React.ComponentPropsWithoutRef<typeof TagsInputPrimitive.Input>
>(({ className, ...props }, ref) => (
  <TagsInputPrimitive.Input
    data-slot="tags-input-input"
    ref={ref}
    className={cn(
      "flex-1 bg-transparent outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
      className,
    )}
    {...props}
  />
));
TagsInputInput.displayName = TagsInputPrimitive.Input.displayName;
 
const TagsInputItem = React.forwardRef<
  React.ComponentRef<typeof TagsInputPrimitive.Item>,
  React.ComponentPropsWithoutRef<typeof TagsInputPrimitive.Item>
>(({ className, children, ...props }, ref) => (
  <TagsInputPrimitive.Item
    data-slot="tags-input-item"
    ref={ref}
    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="h-4 w-4 shrink-0 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100">
      <X className="h-3.5 w-3.5" />
    </TagsInputPrimitive.ItemDelete>
  </TagsInputPrimitive.Item>
));
TagsInputItem.displayName = TagsInputPrimitive.Item.displayName;
 
const TagsInputClear = React.forwardRef<
  React.ComponentRef<typeof TagsInputPrimitive.Clear>,
  React.ComponentPropsWithoutRef<typeof TagsInputPrimitive.Clear>
>(({ className, ...props }, ref) => (
  <TagsInputPrimitive.Clear data-slot="tags-input-clear" ref={ref} {...props} />
));
TagsInputClear.displayName = TagsInputPrimitive.Clear.displayName;
 
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

Kickflip
Heelflip
FS 540
"use client";
 
import * as React from "react";
 
import { Button } from "@/components/ui/button";
import {
  TagsInput,
  TagsInputClear,
  TagsInputInput,
  TagsInputItem,
  TagsInputLabel,
  TagsInputList,
} from "@/components/ui/tags-input";
import { RefreshCcw } from "lucide-react";
 
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.

Add up to 6 tricks with at least 3 characters, excluding "ollie".
"use client";
 
import * as React from "react";
 
import {
  TagsInput,
  TagsInputInput,
  TagsInputItem,
  TagsInputLabel,
  TagsInputList,
} from "@/components/ui/tags-input";
import { toast } from "sonner";
 
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.

The 900
FS 540
"use client";
 
import {
  TagsInput,
  TagsInputInput,
  TagsInputItem,
  TagsInputLabel,
  TagsInputList,
} from "@/components/ui/tags-input";
import * as React from "react";
 
import {
  Sortable,
  SortableContent,
  SortableItem,
  SortableOverlay,
} from "@/components/ui/sortable";
 
import { MouseSensor, TouchSensor, useSensor, useSensors } from "@dnd-kit/core";
 
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.

PropTypeDefault
name?
string
-
readOnly?
boolean
false
required?
boolean
false
max?
number
Number.POSITIVE_INFINITY
delimiter?
string
","
blurBehavior?
"add" | "clear"
-
loop?
boolean
false
editable?
boolean
false
disabled?
boolean
false
addOnTab?
boolean
false
addOnPaste?
boolean
false
displayValue?
((value: string) => string)
-
onValidate?
((value: string) => boolean)
-
asChild?
boolean
-
onResizeCapture?
ReactEventHandler<HTMLDivElement>
-
onResize?
ReactEventHandler<HTMLDivElement>
-
onValueChange?
((value: string[]) => void)
-
value?
string[]
-
defaultValue?
string[]
-
Data AttributeValue
[data-disabled]Present when disabled.
[data-invalid]Present when invalid.
[data-readonly]Present when readOnly.

Label

Label element for the tags input.

PropTypeDefault
asChild?
boolean
-
onResizeCapture?
ReactEventHandler<HTMLLabelElement>
-
onResize?
ReactEventHandler<HTMLLabelElement>
-
Data AttributeValue
[data-disabled]Present when the tags input is disabled.

Input

Text input for adding new tags.

PropTypeDefault
asChild?
boolean
-
onResizeCapture?
ReactEventHandler<HTMLInputElement>
-
onResize?
ReactEventHandler<HTMLInputElement>
-
Data AttributeValue
[data-invalid]Present when the input value is invalid.

Item

Individual tag item.

PropTypeDefault
disabled?
boolean
-
value
string
-
asChild?
boolean
-
onResizeCapture?
ReactEventHandler<HTMLDivElement>
-
onResize?
ReactEventHandler<HTMLDivElement>
-
Data AttributeValue
[data-state]"active" | "inactive"
[data-highlighted]Present when the tag is highlighted.
[data-disabled]Present when the tag is disabled.
[data-editing]Present when the tag is being edited.
[data-editable]Present when the tags input is editable.

ItemText

Text content of a tag.

PropTypeDefault
asChild?
boolean
-
onResizeCapture?
ReactEventHandler<HTMLSpanElement>
-
onResize?
ReactEventHandler<HTMLSpanElement>
-

ItemDelete

Button to remove a tag.

PropTypeDefault
asChild?
boolean
-
onResizeCapture?
ReactEventHandler<HTMLButtonElement>
-
onResize?
ReactEventHandler<HTMLButtonElement>
-
Data AttributeValue
[data-disabled]Present when the delete button is disabled.
[data-state]"active" | "inactive"

Clear

Button to clear all tags.

PropTypeDefault
forceMount?
boolean
false
asChild?
boolean
-
onResizeCapture?
ReactEventHandler<HTMLButtonElement>
-
onResize?
ReactEventHandler<HTMLButtonElement>
-
Data AttributeValue
[data-disabled]Present when the clear button is disabled.
[data-state]"visible" | "invisible"

Accessibility

Keyboard Interactions

KeyDescription
DeleteWhen a tag is highlighted, removes it and sets focus to the next tag.
BackspaceWhen 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.
ArrowLeftHighlights the previous tag. When cursor is at start of input, moves focus to the last tag.
ArrowRightHighlights the next tag. When cursor is at start of input, moves focus to the first tag.
HomeHighlights the first tag in the list.
EndHighlights the last tag in the list.
EnterWhen input has text, adds a new tag. When a tag is highlighted and editable is enabled, enters edit mode.
EscapeClears tag highlight and edit mode, resets cursor to start.
TabWhen `addOnTab` is enabled and input has text, adds a new tag. Otherwise, blurs the input.