88 / Controls
Tag Input
An input that turns what you typed into a removable tag, and gives the last one back when you press backspace on an empty field.
Enter or a comma adds one. Backspace on an empty field takes the last one back.
"use client"
import * as React from "react"
import { TagInput } from "mischief-ui/tag-input"
export function TagInputDemo() {
const [tags, setTags] = React.useState(["design", "typescript"])
return (
<div className="w-full max-w-md">
<TagInput
value={tags}
onChange={setTags}
max={6}
label="Topics"
placeholder="Add a topic"
/>
<p className="text-muted-foreground mt-2 text-xs">
Enter or a comma adds one. Backspace on an empty field takes the last
one back.
</p>
</div>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/tag-inputimport { TagInput } from "mischief-ui/tag-input"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
"use client" import * as React from "react"import { X } from "lucide-react"import { cn } from "@/lib/utils" export type TagInputProps = Omit< React.HTMLAttributes<HTMLDivElement>, "onChange" | "defaultValue"> & { value?: readonly string[] defaultValue?: readonly string[] onChange?: (tags: string[]) => void placeholder?: stringUsage
export function Topics() {
const [tags, setTags] = React.useState(["design"])
return <TagInput value={tags} onChange={setTags} max={6} label="Topics" />
}Every change is said
Adding a tag, removing one, and being refused a duplicate are all announced in a polite live region. Without that, a reader using a screen reader presses Enter and hears nothing, which is indistinguishable from the field being broken.
Each remove control is named with the tag it removes rather than being six identical buttons called Remove.
The habits it expects
- Enter or a comma ends a tag. Whitespace around it is trimmed.
- Backspace on an empty field takes the last tag back, so overshooting is recoverable without reaching for the pointer.
- Leaving the field commits whatever was half typed, rather than throwing it away.
- A duplicate clears the field and says so, instead of silently doing nothing.
API
valuestring[]Controlled tags.defaultValuestring[]Uncontrolled starting tags.onChange(tags: string[]) => voidThe tags after a change.placeholderstringShown in the empty field.separatorsstring[]Keys that end a tag, besides Enter. Defaults to [",", "Enter"].maxnumberMost tags allowed. The field closes once reached.allowDuplicatesbooleanWhether the same tag may be added twice. Off by default.labelstringNames the field. Defaults to "Tags".Accessibility
The field is labelled, described by its status region, and every change is announced politely. Remove controls name their own tag. Clicking the surrounding box focuses the field, and the whole control shows a focus ring when anything inside it has focus.