# Tag Input

A multi-value input with custom tags, suggestions, and keyboard-accessible chips.

Page: https://sui.draco.dev/docs/components/tag-input

### Example: tag-input-demo

```tsx
"use client";

import {
  Field,
  FieldDescription,
  FieldLabel,
} from "@workspace/ui/components/field";
import { TagInput } from "@workspace/ui/components/tag-input";
import { useId } from "react";
import type { ExampleProps } from "../types";

export default function TagInputDemo({ locale }: ExampleProps) {
  const id = useId();
  const chinese = locale === "zh-CN";

  return (
    <Field className="w-full max-w-sm">
      <FieldLabel htmlFor={id}>
        {chinese ? "项目标签" : "Project tags"}
      </FieldLabel>
      <TagInput
        id={id}
        defaultValue={["design", "engineering"]}
        placeholder={chinese ? "添加标签…" : "Add a tag…"}
        labels={
          chinese
            ? {
                input: "项目标签",
                createValue: (value) => `添加“${value}”`,
                removeValue: (value) => `移除 ${value}`,
                empty: "没有建议选项",
              }
            : undefined
        }
      />
      <FieldDescription>
        {chinese
          ? "输入后按 Enter、逗号或 Tab 添加。支持中文输入法"
          : "Add with Enter, comma, or Tab. IME composition stays in the input."}
      </FieldDescription>
    </Field>
  );
}
```

## Installation

```bash
bunx --bun shadcn@latest add @sui/tag-input
```

Install with the shadcn CLI or use the shared `@workspace/ui` package. Follow the [installation guide](/docs/installation) to configure the registry, load styles, and choose import aliases.

## Usage

```tsx
import { TagInput } from "@workspace/ui/components/tag-input";

<TagInput aria-label="Project tags" defaultValue={["design"]} />;
```

Type a tag and press Enter, comma, or Tab. Tags are trimmed; empty values and exact duplicates are ignored. Comparison is case-sensitive. Pasting comma-separated values or multiple lines adds them in order.

The component uses Base UI Combobox's multi-select chips for keyboard navigation and removal. During IME composition, Enter and comma stay with the input method and do not create a tag. With an empty draft, Backspace removes the last tag; arrow keys navigate suggestions and chips.

## Suggestions and controlled values

Supply `suggestions` to offer existing values. Users may still create their own tags by default; set `allowCustom={false}` to restrict new values to the suggestions. A highlighted suggestion is selected with Enter.

Use `value` and `onValueChange` to control the selected array, or `defaultValue` for an uncontrolled input. The draft can also be controlled independently with `inputValue` and `onInputValueChange`.

### Example: tag-input-suggestions

```tsx
"use client";

import {
  Field,
  FieldDescription,
  FieldLabel,
} from "@workspace/ui/components/field";
import { TagInput } from "@workspace/ui/components/tag-input";
import { useId, useState } from "react";
import type { ExampleProps } from "../types";

const suggestions = [
  "React",
  "TypeScript",
  "Fumadocs",
  "TanStack Start",
  "Base UI",
];

export default function TagInputSuggestions({ locale }: ExampleProps) {
  const id = useId();
  const [values, setValues] = useState(["React"]);
  const chinese = locale === "zh-CN";

  return (
    <Field className="w-full max-w-sm">
      <FieldLabel htmlFor={id}>{chinese ? "技术栈" : "Tech stack"}</FieldLabel>
      <TagInput
        id={id}
        value={values}
        onValueChange={setValues}
        suggestions={suggestions}
        placeholder={chinese ? "选择或添加…" : "Choose or add…"}
        labels={
          chinese
            ? {
                input: "技术栈",
                createValue: (value) => `添加“${value}”`,
                removeValue: (value) => `移除 ${value}`,
                empty: "没有匹配的技术",
              }
            : undefined
        }
      />
      <FieldDescription>
        {chinese
          ? "用方向键选择建议，也可以创建自己的标签"
          : "Choose a suggestion with the arrow keys or create your own tag."}
      </FieldDescription>
    </Field>
  );
}
```

## Validation and disabled state

`maxValues` limits the number of tags. `validateValue` checks each new value and receives the values already accepted. Existing tags can always be removed, even when the limit has been reached.

Rejected input remains in the draft and its error is announced. Tab still moves focus out of the input when validation fails; Shift+Tab moves backward without committing the draft. In a batch paste, valid tags before the first rejected value are added, while the remaining values stay in the draft.

`disabled` and `readOnly` prevent changes and disable removal controls.

### Example: tag-input-validation

```tsx
"use client";

import {
  Field,
  FieldDescription,
  FieldLabel,
} from "@workspace/ui/components/field";
import { TagInput } from "@workspace/ui/components/tag-input";
import { useId } from "react";
import type { ExampleProps } from "../types";

export default function TagInputValidation({ locale }: ExampleProps) {
  const id = useId();
  const chinese = locale === "zh-CN";
  const labels = chinese
    ? {
        createValue: (value: string) => `添加“${value}”`,
        removeValue: (value: string) => `移除 ${value}`,
        invalidValue: (value: string) => `“${value}”只能包含字母或数字`,
        maxValuesReached: (limit: number) => `最多添加 ${limit} 个标签`,
        empty: "没有建议选项",
      }
    : undefined;

  return (
    <div className="grid w-full max-w-sm gap-5">
      <Field>
        <FieldLabel htmlFor={`${id}-validated`}>
          {chinese ? "最多 3 个标签" : "Up to 3 tags"}
        </FieldLabel>
        <TagInput
          id={`${id}-validated`}
          defaultValue={["design"]}
          maxValues={3}
          validateValue={(value) => /^[a-z0-9]+$/i.test(value)}
          labels={labels}
          placeholder={chinese ? "添加字母或数字…" : "Letters or numbers…"}
        />
        <FieldDescription>
          {chinese
            ? "错误会保留草稿，移除标签后可继续添加"
            : "Invalid input stays in the draft. Remove a tag to make room."}
        </FieldDescription>
      </Field>
      <Field>
        <FieldLabel htmlFor={`${id}-disabled`}>
          {chinese ? "禁用标签" : "Disabled tags"}
        </FieldLabel>
        <TagInput
          id={`${id}-disabled`}
          defaultValue={["React", "Base UI"]}
          disabled
          labels={labels}
        />
      </Field>
    </div>
  );
}
```

## Composition and forms

Tag Input composes Combobox's root, chips, input, list, and items. Use the standard `Field` components for labels and descriptions. `className` styles the chips container, `inputClassName` styles the draft input, and `ref` points to the native draft input.

Pass `name` to submit each selected tag as a separate form value with the same name. Draft text is not submitted. `required` validates the selected tags, so an existing tag satisfies the requirement even when the draft is empty. Disabled tags are omitted from form data.

Native form reset restores `defaultValue` and `defaultInputValue` in uncontrolled mode. Canceling the reset with `event.preventDefault()` preserves the selected tags and draft. Controlled values remain owned by the application.

### Example: tag-input-form

```tsx
"use client";

import { Button } from "@workspace/ui/components/button";
import { Checkbox } from "@workspace/ui/components/checkbox";
import {
  Field,
  FieldDescription,
  FieldLabel,
} from "@workspace/ui/components/field";
import { TagInput } from "@workspace/ui/components/tag-input";
import { useId, useState } from "react";
import type { ExampleProps } from "../types";

const defaultTags = ["design"];

export default function TagInputForm({ locale }: ExampleProps) {
  const id = useId();
  const [submitted, setSubmitted] = useState<string[] | null>(null);
  const [keepTags, setKeepTags] = useState(false);
  const chinese = locale === "zh-CN";
  const notSubmittedLabel = chinese ? "尚未提交" : "Not submitted";

  return (
    <form
      className="grid w-full max-w-sm gap-4"
      onSubmit={(event) => {
        event.preventDefault();
        const data = new FormData(event.currentTarget);
        setSubmitted(data.getAll("tags").map(String));
      }}
      onReset={(event) => {
        if (keepTags) event.preventDefault();
        else setSubmitted(null);
      }}
    >
      <Field>
        <FieldLabel htmlFor={id}>
          {chinese ? "项目标签" : "Project tags"}
        </FieldLabel>
        <TagInput
          id={id}
          name="tags"
          required
          defaultValue={defaultTags}
          placeholder={chinese ? "添加标签…" : "Add a tag…"}
          labels={
            chinese
              ? {
                  createValue: (value) => `添加“${value}”`,
                  removeValue: (value) => `移除 ${value}`,
                  empty: "没有建议选项",
                }
              : undefined
          }
        />
        <FieldDescription>
          {chinese
            ? "至少选择一个标签。重置会恢复 design；草稿不会进入提交数据"
            : "Choose at least one tag. Reset restores design; draft text is not submitted."}
        </FieldDescription>
      </Field>
      <Field orientation="horizontal">
        <Checkbox
          id={`${id}-keep`}
          checked={keepTags}
          onCheckedChange={setKeepTags}
        />
        <FieldLabel htmlFor={`${id}-keep`}>
          {chinese ? "阻止表单重置" : "Prevent form reset"}
        </FieldLabel>
      </Field>
      <div className="flex gap-2">
        <Button type="submit">{chinese ? "提交" : "Submit"}</Button>
        <Button type="reset" variant="outline">
          {chinese ? "重置" : "Reset"}
        </Button>
      </div>
      <output
        className="rounded-2xl bg-muted px-3 py-2 font-mono text-sm"
        aria-live="polite"
      >
        {submitted === null ? notSubmittedLabel : JSON.stringify(submitted)}
      </output>
    </form>
  );
}
```

## Accessibility

Associate `FieldLabel` with the input's `id`, or provide `aria-label`. Without an `id` or a custom name, the default input name is “Add tag”. Removal buttons have names that include their tag. Validation messages use a status region and are associated with the input.

Localize `labels.input`, `labels.removeValue`, `labels.createValue`, `labels.empty`, `labels.invalidValue`, and `labels.maxValuesReached`. Prefer the same input name as the visible field label.

## API Reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string[]` | — | Controlled selected tags. |
| `defaultValue` | `string[]` | `[]` | Initial uncontrolled tags. |
| `onValueChange` | `(values: string[]) => void` | — | Called when selected tags change. |
| `suggestions` | `readonly string[]` | `[]` | Suggested tags. |
| `allowCustom` | `boolean` | `true` | Allow new values outside suggestions. |
| `maxValues` | `number` | — | Maximum tag count. |
| `validateValue` | `(value: string, values: string[]) => boolean` | — | Validate a new tag. |
| `inputValue` | `string` | — | Controlled draft text. |
| `defaultInputValue` | `string` | `""` | Initial uncontrolled draft text. |
| `onInputValueChange` | `(value: string) => void` | — | Called when the draft changes. |
| `labels` | `TagInputLabels` | English labels | Input, action, and validation text. |
| `inputClassName` | `string` | — | Additional draft input classes. |
| `...props` | Native input props, excluding `value`, `defaultValue`, `type`, and `size` | — | Includes `id`, `ref`, `name`, `form`, `required`, `disabled`, and `readOnly`. |

Keyboard and chip behavior follow the [Base UI Combobox API](https://base-ui.com/react/components/combobox#api-reference).

- [Documentation](https://base-ui.com/react/components/combobox)
- [API reference](https://base-ui.com/react/components/combobox#api-reference)
