Shadcn Tags Input for React and Tailwind
Allows users to add tags to an input field.
Installation
bunx --bun shadcn@latest add https://kit.dev/r/tags-input.jsonpnpm dlx shadcn@latest add https://kit.dev/r/tags-input.jsonnpx shadcn@latest add https://kit.dev/r/tags-input.jsonyarn shadcn@latest add https://kit.dev/r/tags-input.json<Step>This component depends on Input Group. Install it first if you haven't already.</Step>
Install the following dependencies:
bun add @ark-ui/react lucide-reactpnpm add @ark-ui/react lucide-reactnpm install @ark-ui/react lucide-reactyarn add @ark-ui/react lucide-reactCopy and paste the following code into your project.
"use client";
import {
TagsInput as ArkTagsInput,
useTagsInput as useArkTagsInput,
useTagsInputContext as useArkTagsInputContext,
} from "@ark-ui/react/tags-input";
import { XIcon } from "lucide-react";
import type React from "react";
import { cn } from "@/lib/utils";
import {
InputGroup,
InputGroupButton,
InputGroupInput,
type InputGroupProps,
} from "@/components/ui/input-group";
export const useTagsInput = useArkTagsInput;
export const useTagsInputContext = useArkTagsInputContext;
export const TagsInputContext = ArkTagsInput.Context;
interface TagsInputProps
extends React.ComponentProps<typeof ArkTagsInput.Root>,
Pick<InputGroupProps, "size"> {
/**
* Whether to show the clear button.
*
* @default true
*/
showClear?: boolean;
}
export const TagsInput = (props: TagsInputProps) => {
const {
size = "md",
showClear,
editable = false,
tabIndex,
className,
children,
...rest
} = props;
return (
<ArkTagsInput.Root
className={cn(
"group/tags-input",
"flex w-full flex-col gap-2",
className
)}
data-size={size}
data-slot="tags-input"
editable={editable}
{...rest}
>
<TagsInputControl showClear={showClear}>
{children}
<TagsInputInput placeholder="Add framework" />
</TagsInputControl>
<ArkTagsInput.HiddenInput tabIndex={tabIndex} />
</ArkTagsInput.Root>
);
};
interface TagsInputControlProps
extends React.ComponentProps<typeof ArkTagsInput.Control>,
Pick<InputGroupProps, "size"> {
/**
* Whether to show the clear button.
*
* @default true
*/
showClear?: boolean;
}
export const TagsInputControl = (props: TagsInputControlProps) => {
const { size, showClear = true, className, children, ...rest } = props;
const api = useTagsInputContext();
return (
<ArkTagsInput.Control asChild data-slot="tags-input-control">
<InputGroup
className={cn(
"h-auto in-data-[size=lg]:min-h-9 in-data-[size=sm]:min-h-7 min-h-8",
"p-1",
"flex-wrap content-start items-center gap-1",
"data-disabled:pointer-events-none data-disabled:opacity-64",
className
)}
size={size}
{...rest}
>
{children}
{showClear && api.value.length > 0 && (
<TagsInputClearTrigger aria-label="Clear all tags" />
)}
</InputGroup>
</ArkTagsInput.Control>
);
};
interface TagsInputItemProps
extends React.ComponentProps<typeof ArkTagsInput.Item>,
Pick<InputGroupProps, "size"> {
/**
* Whether to show the clear trigger.
*
* @default true
*/
showDelete?: boolean;
}
export const TagsInputItem = (props: TagsInputItemProps) => {
const { showDelete = true, className, children, ...rest } = props;
return (
<ArkTagsInput.Item
className={cn(
"h-6 in-data-[size=lg]:h-7 in-data-[size=sm]:h-5 max-w-full",
"pr-0.5 in-data-[size=lg]:pl-2 in-data-[size=sm]:pl-1 pl-1.5",
"inline-flex shrink-0 items-center gap-1",
"bg-secondary",
"in-data-[size=lg]:text-sm text-secondary-foreground text-xs",
"rounded-md border outline-none",
"data-highlighted:border-primary/30 data-highlighted:bg-primary/10",
className
)}
data-slot="tags-input-item"
{...rest}
>
<TagsInputItemPreview>
<TagsInputItemText>{children}</TagsInputItemText>
{showDelete && <TagsInputItemDeleteTrigger />}
</TagsInputItemPreview>
<TagsInputItemInput />
</ArkTagsInput.Item>
);
};
export const TagsInputItemPreview = (
props: React.ComponentProps<typeof ArkTagsInput.ItemPreview>
) => {
const { className, ...rest } = props;
return (
<ArkTagsInput.ItemPreview
className={cn("inline-flex max-w-full items-center gap-1", className)}
data-slot="tags-input-item-preview"
{...rest}
/>
);
};
export const TagsInputItemText = (
props: React.ComponentProps<typeof ArkTagsInput.ItemText>
) => {
const { className, ...rest } = props;
return (
<ArkTagsInput.ItemText
className={cn("truncate", className)}
data-slot="tags-input-item-text"
{...rest}
/>
);
};
export const TagsInputItemDeleteTrigger = (
props: React.ComponentProps<typeof ArkTagsInput.ItemDeleteTrigger>
) => {
const { className, children, ...rest } = props;
return (
<ArkTagsInput.ItemDeleteTrigger
asChild
data-slot="tags-input-item-delete-trigger"
{...rest}
>
<InputGroupButton
className={cn(
"in-data-[size=lg]:size-6 in-data-[size=sm]:size-4 size-5",
"shrink-0",
"text-muted-foreground",
"rounded-[calc(var(--radius)-5px)]",
"[&_svg:not([class*='size-'])]:size-3",
"hover:text-foreground",
className
)}
size="icon-xs"
variant="ghost"
>
{children ?? <XIcon aria-hidden />}
</InputGroupButton>
</ArkTagsInput.ItemDeleteTrigger>
);
};
export const TagsInputItemInput = (
props: React.ComponentProps<typeof ArkTagsInput.ItemInput>
) => (
<ArkTagsInput.ItemInput asChild data-slot="tags-input-item-input" {...props}>
<InputGroupInput
className={cn(
"px-1 text-xs",
"h-6 in-data-[size=lg]:h-7 in-data-[size=sm]:h-5"
)}
/>
</ArkTagsInput.ItemInput>
);
export const TagsInputInput = (
props: React.ComponentProps<typeof ArkTagsInput.Input>
) => (
<ArkTagsInput.Input asChild data-slot="tags-input-input" {...props}>
<InputGroupInput
className={cn(
"w-auto min-w-18 max-w-full flex-auto shrink basis-auto",
"h-7 in-data-[size=lg]:h-8 in-data-[size=sm]:h-6"
)}
/>
</ArkTagsInput.Input>
);
export const TagsInputClearTrigger = (
props: React.ComponentProps<typeof ArkTagsInput.ClearTrigger>
) => {
const { className, children, ...rest } = props;
return (
<ArkTagsInput.ClearTrigger
asChild
data-slot="tags-input-clear-trigger"
{...rest}
>
<InputGroupButton
className={cn(
"ms-auto shrink-0 self-center text-muted-foreground hover:text-foreground",
className
)}
size="icon-xs"
variant="ghost"
>
{children ?? <XIcon aria-hidden />}
</InputGroupButton>
</ArkTagsInput.ClearTrigger>
);
};
interface TagsInputRootProviderProps
extends React.ComponentProps<typeof ArkTagsInput.RootProvider>,
Pick<InputGroupProps, "size"> {
/**
* Whether to show the clear button.
*
* @default true
*/
showClear?: boolean;
}
export const TagsInputRootProvider = (props: TagsInputRootProviderProps) => {
const { size = "md", showClear, className, children, ...rest } = props;
return (
<ArkTagsInput.RootProvider
className={cn(
"group/tags-input",
"flex w-full flex-col gap-2",
className
)}
data-size={size}
data-slot="tags-input-root-provider"
{...rest}
>
<TagsInputControl showClear={showClear}>{children}</TagsInputControl>
<ArkTagsInput.HiddenInput />
</ArkTagsInput.RootProvider>
);
};Update the import paths to match your project setup.
Anatomy
TagsInput
└── TagsInputContext
└── TagsInputItemUsage
import {
TagsInput,
TagsInputContext,
TagsInputItem,
} from "@/components/ui/tags-input";<TagsInput defaultValue={["React", "Solid"]}>
<TagsInputContext>
{({ value }) =>
value.map((value, index) => (
<TagsInputItem index={index} key={value} value={value}>
{value}
</TagsInputItem>
))
}
</TagsInputContext>
</TagsInput>Controlled
Use value and onValueChange to control the tags array.
States
Disabled
Use the disabled prop to disable the tags input. Users cannot add, remove, or edit tags.
Invalid
Use the invalid prop to mark the tags input as invalid for form validation.
Sizes
Use the size prop on TagsInput to change the control and input height.
Small
Medium
Large
Examples
Blur Behavior
Use blurBehavior="add" to add the current input value as a tag when the field loses focus.
Controlled Input Value
Use inputValue and onInputValueChange to control the text field independently.
Delimiter
Use delimiter with a regex to split tags when typing or pasting separators.
Disabled Editing
Pass editable={false} to prevent editing existing tags after creation.
Max Tag Length
Use maxLength to cap characters per tag.
Max With Overflow
Use max with allowOverflow to allow exceeding the limit while marking the root as invalid.
Paste Behavior
Use addOnPaste with delimiter to create multiple tags from pasted text.
Sanitize
Use sanitizeValue to normalize tag text before tags are added.
Validation
Use validate to accept or reject tags before they are added.
With Combobox
With Field
Wrap with Field for labels, descriptions, and validation messaging.
API Reference
TagsInput
Root component. Composes the control, input, and hidden field for form submission.
| Prop | Type | Default |
|---|---|---|
size | "sm" | "md" | "lg" | "md" |
showClear | boolean | true |
defaultValue | string[] | - |
value | string[] | - |
inputValue | string | - |
defaultInputValue | string | - |
max | number | Infinity |
maxLength | number | - |
delimiter | string | RegExp | "," |
allowDuplicates | boolean | false |
allowOverflow | boolean | - |
disabled | boolean | - |
invalid | boolean | - |
required | boolean | - |
editable | boolean | false |
addOnPaste | boolean | false |
blurBehavior | "add" | "clear" | - |
validate | (details) => boolean | - |
sanitizeValue | (value: string) => string | (value) => value.trim() |
onValueChange | (details) => void | - |
onInputValueChange | (details) => void | - |
onHighlightChange | (details) => void | - |
onValueInvalid | (details) => void | - |
className | string | - |
TagsInputInput
Borderless text field for adding new tags. Rendered inside TagsInput by default.
| Prop | Type | Default |
|---|---|---|
placeholder | string | - |
className | string | - |
TagsInputItem
Represents a single tag. Requires index and value. Pass the label as children. Renders the delete trigger and edit input. Chip height follows the root size (sm, md, lg). Supports data-highlighted when navigated by keyboard.
| Prop | Type | Default |
|---|---|---|
index | number | required |
value | string | required |
showDelete | boolean | true |
disabled | boolean | - |
className | string | - |
TagsInputClearTrigger
Clears all tags.
| Prop | Type | Default |
|---|---|---|
className | string | - |
asChild | boolean | - |
For the full list of props and context methods, see the Ark UI documentation.