ComponentsTagInput
TagInput
Let users add, edit, and remove text tags.
On this page
Install component source from the public registry, or use the local workspace package. Follow the setup guide first. The npm package is not published yet.
View registry files- React Aria Components
(react-aria-components)Accessible interaction primitives, including keyboard behavior, focus handling, and ARIA semantics.
Shared foundation: React, clsx for conditional classes, tailwind-merge for resolving utility-class conflicts. Styles use Tailwind CSS and Dethink CSS variables.
Based on this component’s source and registry composition. Libraries used only in showcase examples are excluded.
Import the component into your page or component file.
import { TagInput } from "@dethink/components";Type a value and press Enter, comma, or Tab. Backspace removes the previous chip when the input is empty.
Basic
Free-form labels with helper copy and native form serialization.
Press Enter, comma, or Tab to add a label.
Press Enter, comma, or Tab to add a label.
Show sourceexamples/tag-input/basic.tsx
"use client";
import { TagInput } from "@dethink/components";
export function TagInputBasic() {
return (
<div className="mx-auto max-w-sm">
<TagInput
description="Press Enter, comma, or Tab to add a label."
label="Labels"
name="labels"
placeholder="Add label"
/>
</div>
);
}Controlled
Apps can own the tag array and render derived submission state.
Serialized tags: finance
Show sourceexamples/tag-input/controlled.tsx
"use client";
import { useState } from "react";
import { FieldDescription, Stack, TagInput } from "@dethink/components";
export function TagInputControlled() {
const [tags, setTags] = useState(["finance"]);
return (
<div className="mx-auto max-w-sm">
<Stack gap="3">
<TagInput
label="Notification tags"
name="notificationTags"
onValueChange={setTags}
value={tags}
/>
<FieldDescription>
Serialized tags: {tags.join(", ") || "none"}
</FieldDescription>
</Stack>
</div>
);
}Validation
Duplicate prevention is built in; custom validation blocks tags with a visible message.
Tags must start with # and stay short.
Tags must start with # and stay short.
Show sourceexamples/tag-input/validation.tsx
"use client";
import { TagInput } from "@dethink/components";
export function TagInputValidation() {
return (
<div className="mx-auto max-w-sm">
<TagInput
defaultValue={["#finance"]}
description="Tags must start with # and stay short."
label="Campaign tags"
maxTagLength={16}
name="campaignTags"
validateTag={(value) =>
value.startsWith("#") ? null : "Tags must start with #."
}
/>
</div>
);
}Form states
Invalid, required, read-only, and disabled states use the same field styling contract as other inputs.
Add at least one recipient tag.
Add at least one recipient tag.
Show sourceexamples/tag-input/states.tsx
"use client";
import { Stack, TagInput } from "@dethink/components";
export function TagInputStates() {
return (
<div className="mx-auto max-w-sm">
<Stack gap="4">
<TagInput
errorMessage="Add at least one recipient tag."
invalid
label="Recipient tags"
required
/>
<TagInput
readOnly
defaultValue={["finance", "renewal"]}
label="Inherited labels"
/>
<TagInput disabled defaultValue={["locked"]} label="Locked labels" />
</Stack>
</div>
);
}Theme, RTL, and wrapping
Nested provider tokens, compact density, RTL direction, and narrow chip wrapping.
Show sourceexamples/tag-input/theme-and-wrapping.tsx
"use client";
import { DethinkProvider, TagInput } from "@dethink/components";
export function TagInputThemeAndWrapping() {
return (
<DethinkProvider theme="dark" density="compact" dir="rtl">
<div className="border-border bg-background mx-auto max-w-72 rounded-lg border p-4">
<TagInput
defaultValue={[
"finance",
"renewal",
"executive-review",
"customer-success",
]}
label="وسوم الحساب"
name="accountTags"
/>
</div>
</DethinkProvider>
);
}Examples that combine components for common tasks.
Case label editor
A compact form with a capped label list and a second TagInput validating email recipients.
Show sourceexamples/tag-input/recipe-label-editor.tsx
"use client";
import { Form, Stack, TagInput } from "@dethink/components";
export function TagInputRecipeLabelEditor() {
return (
<Form action="/cases" method="post" className="mx-auto max-w-md">
<div className="border-border bg-muted/20 rounded-lg border p-4">
<Stack gap="4">
<TagInput
defaultValue={["finance", "renewal", "priority"]}
description="Paste comma-separated values to add several labels at once."
label="Case labels"
name="caseLabels"
maxTags={8}
maxTagLength={24}
/>
<TagInput
defaultValue={["ops@example.com"]}
label="Notification recipients"
name="recipients"
validateTag={(value) =>
value.includes("@") ? null : "Use an email address."
}
/>
</Stack>
</div>
</Form>
);
}TagInput owns its label, chip list, entry input, validation, and repeated form fields.
| Prop | What it does | Default |
|---|---|---|
value / defaultValue / onValueChangestring[] / string[] / (value) => void | Controlled or uncontrolled tags. | [] |
inputValue / defaultInputValue / onInputValueChangestring / string / (text) => void | Controlled or uncontrolled text for the entry field. | "" |
delimitersstring[] | Characters that commit the current text as a tag. | [,] |
normalizeTag(value) => string | Normalizes typed and initial tag values before validation. | trim + collapse spaces |
validateTag(value, existing) => ReactNode | null | Returns a message to block a tag, or null to accept it. | Not set |
maxTags / maxTagLengthnumber | Limits total tag count and individual tag length. | Not set |
label / description / errorMessageReactNode | The field label, help text, and error message. | Not set |
placeholder / removeLabelstring | Entry placeholder and remove-button action text. React Aria appends the tag label. | "Add tag" / "Remove" |
controlSize"sm" | "md" | "lg" | Control height and typography scale. | "md" |
required / disabled / readOnly / invalidboolean | Makes the field required, disabled, read-only, or invalid. | false |
namestring | Repeated hidden input name for native form submission. | Not set |