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.
Prop | Type | Default |
---|---|---|
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 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 | Default |
---|---|---|
asChild? | boolean | - |
onResizeCapture? | ReactEventHandler<HTMLLabelElement> | - |
onResize? | ReactEventHandler<HTMLLabelElement> | - |
Data Attribute | Value |
---|---|
[data-disabled] | Present when the tags input is disabled. |
Input
Text input for adding new tags.
Prop | Type | Default |
---|---|---|
asChild? | boolean | - |
onResizeCapture? | ReactEventHandler<HTMLInputElement> | - |
onResize? | ReactEventHandler<HTMLInputElement> | - |
Data Attribute | Value |
---|---|
[data-invalid] | Present when the input value is invalid. |
Item
Individual tag item.
Prop | Type | Default |
---|---|---|
disabled? | boolean | - |
value | string | - |
asChild? | boolean | - |
onResizeCapture? | ReactEventHandler<HTMLDivElement> | - |
onResize? | ReactEventHandler<HTMLDivElement> | - |
Data Attribute | Value |
---|---|
[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.
Prop | Type | Default |
---|---|---|
asChild? | boolean | - |
onResizeCapture? | ReactEventHandler<HTMLSpanElement> | - |
onResize? | ReactEventHandler<HTMLSpanElement> | - |
ItemDelete
Button to remove a tag.
Prop | Type | Default |
---|---|---|
asChild? | boolean | - |
onResizeCapture? | ReactEventHandler<HTMLButtonElement> | - |
onResize? | ReactEventHandler<HTMLButtonElement> | - |
Data Attribute | Value |
---|---|
[data-disabled] | Present when the delete button is disabled. |
[data-state] | "active" | "inactive" |
Clear
Button to clear all tags.
Prop | Type | Default |
---|---|---|
forceMount? | boolean | false |
asChild? | boolean | - |
onResizeCapture? | ReactEventHandler<HTMLButtonElement> | - |
onResize? | ReactEventHandler<HTMLButtonElement> | - |
Data Attribute | Value |
---|---|
[data-disabled] | Present when the clear button is 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. |