ComponentsTagInput
TagInput
Let users add, edit, and remove text tags.
On this page
Use this component from the local workspace package. Follow the setup guide first. A public npm package and registry are not available yet.
View registry filesImport 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 |