# Tag Input

A text field that turns what's typed into a list of removable tags.

Source: [tag-input.tsx](https://github.com/jakejarvis/jarvis-ui/blob/main/src/components/ui/tag-input.tsx)

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

export default function TagInputBasic() {
  return (
    <TagInput defaultValue={["Design", "Research"]} className="max-w-sm">
      <TagInputList />
      <TagInputInput aria-label="Topics" placeholder="Add a topic" />
    </TagInput>
  );
}
```

## Installation

Add the registry, once per project:

```bash
npx shadcn@latest registry add @jarvis=https://ui.jarv.is/r/{name}.json
```

Then add the component:

```bash
npx shadcn@latest add @jarvis/tag-input
```

## Usage

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

export function Topics() {
  return (
    <TagInput defaultValue={["Design"]}>
      <TagInputList />
      <TagInputInput aria-label="Topics" placeholder="Add a topic" />
    </TagInput>
  );
}
```

`TagInput` holds the tags as strings and draws the box around them. `TagInputList` renders a
`TagInputItem` for each tag, or calls its child for each one. Enter adds what's been typed, and so
does typing a comma. Pass `delimiters` to use other characters in its place. Pasted text is split on
them too, and on new lines and tabs, so a pasted list becomes a tag per line, and any it turns away
stay in the input to fix. Tags are trimmed, with runs of spaces collapsed, and a tag that's already
there, ignoring case, is turned away unless you set `allowDuplicates`.

Backspace with the caret at the start of the input highlights the last tag, and a second Backspace
removes it. While a tag is highlighted, the left and right arrows, Home and End move between tags,
Delete removes it and Escape lets it go. Each change is announced to screen readers.

To suggest tags as people type, pass `suggestions` and add a `TagInputContent`, as below. For a
single value, use an [Autocomplete](https://ui.jarv.is/components/autocomplete.md), which suggests the same way. To pick
several from a short, fixed list, a [Combobox](https://ui.jarv.is/components/combobox.md) with `multiple` also works.

## Composition

- `TagInput`
  - `TagInputList`
    - `TagInputItem`
  - `TagInputInput`
  - `TagInputClear`
  - `TagInputContent`
    - `TagInputStatus`
    - `TagInputEmpty`
    - `TagInputSuggestions`
      - `TagInputSuggestion`

## Examples

### With a label

Put it in a `Field`, and point a `FieldLabel` at the input's `id`.

```tsx
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import { TagInput, TagInputInput, TagInputList } from "@/components/ui/tag-input";

export default function TagInputWithField() {
  return (
    <Field className="max-w-sm">
      <FieldLabel htmlFor="skills">Skills</FieldLabel>
      <TagInput defaultValue={["TypeScript", "React"]}>
        <TagInputList />
        <TagInputInput id="skills" placeholder="Add a skill" />
      </TagInput>
      <FieldDescription>Press Enter or type a comma to add one.</FieldDescription>
    </Field>
  );
}
```

### Limit

`max` caps the number of tags. Control the value to show how many are left.

```tsx
import * as React from "react";

import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import { TagInput, TagInputInput, TagInputList } from "@/components/ui/tag-input";

const max = 4;

export default function TagInputMax() {
  const [labels, setLabels] = React.useState(["Launch", "Q3"]);

  return (
    <Field className="max-w-sm">
      <FieldLabel htmlFor="labels">Labels</FieldLabel>
      <TagInput value={labels} onValueChange={setLabels} max={max}>
        <TagInputList />
        <TagInputInput id="labels" placeholder={labels.length < max ? "Add a label" : undefined} />
      </TagInput>
      <FieldDescription className="tabular-nums">
        {labels.length} of {max} labels
      </FieldDescription>
    </Field>
  );
}
```

### Validation

`validate` decides which tags to take, and `onTagReject` tells you which it turned away and why:
`validate` returned false, the tag's a `duplicate`, there were `max` already, or it's `unlisted`
among the suggestions, as below. Set `invalid` on
the `Field` to show the error. Here spaces separate addresses as well as commas, and `addOnBlur`
adds an address left in the input when focus leaves it.

```tsx
import * as React from "react";

import { Field, FieldDescription, FieldError, FieldLabel } from "@/components/ui/field";
import { TagInput, TagInputInput, TagInputList } from "@/components/ui/tag-input";

const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

export default function TagInputValidation() {
  const [error, setError] = React.useState<string | null>(null);

  return (
    <Field invalid={!!error} className="max-w-sm">
      <FieldLabel htmlFor="invite">Invite people</FieldLabel>
      <TagInput
        defaultValue={["ada@example.com"]}
        delimiters={[",", ";", " "]}
        validate={(tag) => emailPattern.test(tag)}
        addOnBlur
        onValueChange={() => setError(null)}
        onTagReject={(tag, reason) =>
          setError(
            reason === "duplicate"
              ? `${tag} is already on the list.`
              : `${tag} isn’t an email address.`,
          )
        }
      >
        <TagInputList />
        <TagInputInput id="invite" inputMode="email" placeholder="name@example.com" />
      </TagInput>
      {error ? (
        <FieldError>{error}</FieldError>
      ) : (
        <FieldDescription>
          Separate addresses with commas or spaces, or paste a list.
        </FieldDescription>
      )}
    </Field>
  );
}
```

### Editable

With `editable`, double-clicking a tag, or pressing Enter while it's highlighted, edits it in place.
Enter or leaving the tag saves it, Escape cancels, and clearing it removes the tag. When the new
text is turned away, Enter keeps the tag open to fix it, and leaving it puts the old text back.

```tsx
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import { TagInput, TagInputInput, TagInputList } from "@/components/ui/tag-input";

export default function TagInputEditable() {
  return (
    <Field className="max-w-sm">
      <FieldLabel htmlFor="areas">Product areas</FieldLabel>
      <TagInput editable defaultValue={["Onboarding", "Billing", "Mobile app"]}>
        <TagInputList />
        <TagInputInput id="areas" placeholder="Add an area" />
      </TagInput>
      <FieldDescription>Double-click a tag to rename it.</FieldDescription>
    </Field>
  );
}
```

### Suggestions

Pass the tags to suggest as `suggestions`, and put a `TagInputSuggestions` in a `TagInputContent`.
The list opens as people type, or on the down arrow, and leaves out the tags already added. The up
and down arrows move through it, and Enter or a click adds the highlighted suggestion. With none
highlighted, Enter adds what's been typed, so people can still add a tag that isn't suggested, and
typing one that matches a suggestion but for its case adds the suggestion's spelling. Set
`autoHighlight` to highlight the first match as people type, so Enter adds it instead.

```tsx
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import {
  TagInput,
  TagInputContent,
  TagInputInput,
  TagInputList,
  TagInputSuggestions,
} from "@/components/ui/tag-input";

const languages = [
  "C#",
  "Elixir",
  "Go",
  "Java",
  "JavaScript",
  "Kotlin",
  "PHP",
  "Python",
  "Ruby",
  "Rust",
  "Swift",
  "TypeScript",
];

export default function TagInputWithSuggestions() {
  return (
    <Field className="max-w-sm">
      <FieldLabel htmlFor="languages">Languages</FieldLabel>
      <TagInput suggestions={languages} defaultValue={["TypeScript"]}>
        <TagInputList />
        <TagInputInput id="languages" placeholder="Add a language" />
        <TagInputContent>
          <TagInputSuggestions />
        </TagInputContent>
      </TagInput>
      <FieldDescription>Pick a suggestion, or type any language and press Enter.</FieldDescription>
    </Field>
  );
}
```

### Custom suggestions

Pass `TagInputSuggestions` a function to render each suggestion. `suggestionLabel` gives the text
people's typing matches, here a person's name as well as their email address, and `autoHighlight`
lets Enter add the first person who matches. A `TagInputEmpty` shows when nothing matches. Without
one, the list closes.

```tsx
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Field, FieldLabel } from "@/components/ui/field";
import {
  TagInput,
  TagInputContent,
  TagInputEmpty,
  TagInputInput,
  TagInputItem,
  TagInputList,
  TagInputSuggestion,
  TagInputSuggestions,
} from "@/components/ui/tag-input";
import { people } from "@/examples/data";

const emails = people.map((person) => person.email);
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

function findPerson(email: string) {
  return people.find((person) => person.email === email);
}

function personLabel(email: string) {
  const person = findPerson(email);
  return person ? `${person.name} ${email}` : email;
}

export default function TagInputCustomSuggestions() {
  return (
    <Field className="max-w-sm">
      <FieldLabel htmlFor="reviewers">Reviewers</FieldLabel>
      <TagInput
        suggestions={emails}
        suggestionLabel={personLabel}
        autoHighlight
        validate={(email) => emailPattern.test(email)}
        defaultValue={[people[0].email]}
      >
        <TagInputList>
          {(email) => {
            const person = findPerson(email);
            return (
              <TagInputItem className="pl-0.5">
                <Avatar className="size-4 text-[8px]">
                  <AvatarImage src={person?.image} alt="" />
                  <AvatarFallback>{person?.initials ?? email[0]}</AvatarFallback>
                </Avatar>
                <span className="truncate">{person?.name ?? email}</span>
              </TagInputItem>
            );
          }}
        </TagInputList>
        <TagInputInput id="reviewers" placeholder="Add a reviewer" />
        <TagInputContent>
          <TagInputEmpty>No one matches. Type a full email address to add it.</TagInputEmpty>
          <TagInputSuggestions>
            {(email) => {
              const person = findPerson(email);
              return (
                <TagInputSuggestion key={email} value={email} className="h-auto py-1.5">
                  <Avatar size="sm">
                    <AvatarImage src={person?.image} alt="" />
                    <AvatarFallback>{person?.initials}</AvatarFallback>
                  </Avatar>
                  <span className="flex min-w-0 flex-col">
                    <span className="truncate">{person?.name}</span>
                    <span className="truncate text-2xs text-muted-foreground">{email}</span>
                  </span>
                </TagInputSuggestion>
              );
            }}
          </TagInputSuggestions>
        </TagInputContent>
      </TagInput>
    </Field>
  );
}
```

### Async suggestions

To load suggestions from somewhere else, such as a search endpoint, pass `loadSuggestions` in place
of `suggestions`. It's called with the typed text once people stop typing for `loadDelay`
milliseconds, 300 by default, and with an `AbortSignal` that aborts when they type again, so pass
it on to `fetch`. When it reads anything besides the typed text, such as a selected category, list
that in `loadDeps` so suggestions load again when it changes.

```tsx
import {
  TagInput,
  TagInputContent,
  TagInputEmpty,
  TagInputInput,
  TagInputList,
  TagInputStatus,
  TagInputSuggestions,
} from "@/components/ui/tag-input";

async function searchTopics(query: string, { signal }: { signal: AbortSignal }) {
  const response = await fetch(`/api/topics?q=${encodeURIComponent(query)}`, { signal });
  return (await response.json()) as string[];
}

export function Topics() {
  return (
    <TagInput loadSuggestions={searchTopics}>
      <TagInputList />
      <TagInputInput aria-label="Topics" placeholder="Search topics" />
      <TagInputContent>
        <TagInputStatus />
        <TagInputEmpty>No topics match.</TagInputEmpty>
        <TagInputSuggestions />
      </TagInputContent>
    </TagInput>
  );
}
```

The results are shown as given, less the tags already added. A `TagInputStatus` shows a spinner
while they load and a message if loading fails, and screen readers announce both. Until new results
arrive, the last ones stay in the list, dimmed. Here a stand-in for the endpoint answers after 600
milliseconds with the libraries that start with what's typed.

```tsx
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import {
  TagInput,
  TagInputContent,
  TagInputEmpty,
  TagInputInput,
  TagInputList,
  TagInputStatus,
  TagInputSuggestions,
} from "@/components/ui/tag-input";
import { searchLibraries } from "@/examples/data";

export default function TagInputAsyncSuggestions() {
  return (
    <Field className="max-w-sm">
      <FieldLabel htmlFor="libraries">Libraries</FieldLabel>
      <TagInput loadSuggestions={searchLibraries}>
        <TagInputList />
        <TagInputInput id="libraries" placeholder="Search libraries" />
        <TagInputContent>
          <TagInputStatus />
          <TagInputEmpty>No libraries match.</TagInputEmpty>
          <TagInputSuggestions />
        </TagInputContent>
      </TagInput>
      <FieldDescription>Suggestions load as you type, such as "re".</FieldDescription>
    </Field>
  );
}
```

### Only suggestions

Set `allowCustom={false}` to take only tags among the suggestions. Typed text that matches one,
ignoring case, adds it with the suggestion's spelling, and `autoHighlight` is on, so Enter adds the
first match. Anything else stays in the input, and `onTagReject` gets it with the reason
`unlisted`.

```tsx
import * as React from "react";

import { Field, FieldDescription, FieldError, FieldLabel } from "@/components/ui/field";
import {
  TagInput,
  TagInputContent,
  TagInputEmpty,
  TagInputInput,
  TagInputList,
  TagInputSuggestions,
} from "@/components/ui/tag-input";

const labels = ["Bug", "Design", "Documentation", "Feature", "Performance", "Security"];

export default function TagInputOnlySuggestions() {
  const [error, setError] = React.useState<string | null>(null);

  return (
    <Field invalid={!!error} className="max-w-sm">
      <FieldLabel htmlFor="issue-labels">Labels</FieldLabel>
      <TagInput
        suggestions={labels}
        allowCustom={false}
        defaultValue={["Bug"]}
        onValueChange={() => setError(null)}
        onTagReject={(tag, reason) =>
          setError(
            reason === "unlisted"
              ? `There’s no “${tag}” label.`
              : `${tag} is already on the issue.`,
          )
        }
      >
        <TagInputList />
        <TagInputInput id="issue-labels" placeholder="Add a label" />
        <TagInputContent>
          <TagInputEmpty>No labels match.</TagInputEmpty>
          <TagInputSuggestions />
        </TagInputContent>
      </TagInput>
      {error ? (
        <FieldError>{error}</FieldError>
      ) : (
        <FieldDescription>Choose from the project’s labels.</FieldDescription>
      )}
    </Field>
  );
}
```

It works with `loadSuggestions` too, where any result it's returned counts. When Enter comes before
the results for what's typed, it waits for them, then adds the first one that isn't already a tag,
the same one it would have added had they been there, or turns the text away when there's none:

```tsx
import {
  TagInput,
  TagInputContent,
  TagInputInput,
  TagInputList,
  TagInputStatus,
  TagInputSuggestions,
} from "@/components/ui/tag-input";

async function searchTopics(query: string, { signal }: { signal: AbortSignal }) {
  const response = await fetch(`/api/topics?q=${encodeURIComponent(query)}`, { signal });
  return (await response.json()) as string[];
}

export function Topics() {
  return (
    <TagInput loadSuggestions={searchTopics} allowCustom={false}>
      <TagInputList />
      <TagInputInput aria-label="Topics" placeholder="Search topics" />
      <TagInputContent>
        <TagInputStatus />
        <TagInputSuggestions />
      </TagInputContent>
    </TagInput>
  );
}
```

### Custom tags

Pass `TagInputList` a function to render each tag yourself. A `TagInputClear` removes them all.

```tsx
import { HashIcon } from "lucide-react";

import {
  TagInput,
  TagInputClear,
  TagInputInput,
  TagInputItem,
  TagInputList,
} from "@/components/ui/tag-input";

export default function TagInputCustomItems() {
  return (
    <TagInput defaultValue={["release", "changelog", "v2"]} className="max-w-sm">
      <TagInputList>
        {(tag) => (
          <TagInputItem>
            <HashIcon />
            <span className="truncate">{tag}</span>
          </TagInputItem>
        )}
      </TagInputList>
      <TagInputInput aria-label="Hashtags" placeholder="Add a hashtag" />
      <TagInputClear />
    </TagInput>
  );
}
```

### In a form

Give a `Field` or the tag input a `name`, and each tag submits under it in order, so
`formData.getAll(name)` returns them, and a `Form`'s `onFormSubmit` gets them as an array. Text typed
that isn't a tag yet never submits. Pass `form` with a form's `id` when the tag input sits outside
it. With `required`, the form won't submit without a tag, which fails as `valueMissing`. `addOnBlur`
adds text left in the input as focus moves to the submit button. A form reset brings back
`defaultValue`.

```tsx
import * as React from "react";

import { Button } from "@/components/ui/button";
import { Field, FieldDescription, FieldError, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { TagInput, TagInputInput, TagInputList } from "@/components/ui/tag-input";

type State = { topics: string[]; error?: string; saved?: boolean };

async function saveTopics(_state: State, formData: FormData): Promise<State> {
  await new Promise((resolve) => setTimeout(resolve, 600));
  const topics = formData.getAll("topics").map(String);
  if (topics.length === 0) return { topics, error: "Add at least one topic." };
  return { topics, saved: true };
}

export default function TagInputForm() {
  const [state, action, pending] = React.useActionState(saveTopics, {
    topics: ["Motion", "Typography"],
  });

  return (
    <Form className="max-w-sm" action={action}>
      <Field name="topics" invalid={!!state.error}>
        <FieldLabel>Topics</FieldLabel>
        <TagInput defaultValue={state.topics} addOnBlur required>
          <TagInputList />
          <TagInputInput placeholder="Add a topic" />
        </TagInput>
        <FieldDescription>Each topic submits as its own value.</FieldDescription>
        <FieldError match="valueMissing">Add at least one topic.</FieldError>
        {state.error && <FieldError>{state.error}</FieldError>}
      </Field>
      <div className="flex items-center gap-3">
        <Button type="submit" variant="primary" disabled={pending}>
          Save topics
        </Button>
        {state.saved && (
          <p role="status" className="text-sm text-muted-foreground">
            Topics saved.
          </p>
        )}
      </div>
    </Form>
  );
}
```

### Sizes

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

export default function TagInputSizes() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-4">
      <TagInput defaultValue={["Default"]}>
        <TagInputList />
        <TagInputInput aria-label="Default size" placeholder="Add a tag" />
      </TagInput>
      <TagInput size="lg" defaultValue={["Large"]}>
        <TagInputList />
        <TagInputInput aria-label="Large size" placeholder="Add a tag" />
      </TagInput>
    </div>
  );
}
```

### Disabled and read-only

A read-only tag input shows its tags without remove buttons, and turns away new ones. A disabled
`Field` disables the tag input inside it too.

```tsx
import { Field, FieldLabel } from "@/components/ui/field";
import { TagInput, TagInputInput, TagInputList } from "@/components/ui/tag-input";

export default function TagInputDisabled() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-6">
      <Field disabled>
        <FieldLabel htmlFor="disabled-regions">Regions</FieldLabel>
        <TagInput defaultValue={["us-east-1", "eu-west-1"]}>
          <TagInputList />
          <TagInputInput id="disabled-regions" placeholder="Add a region" />
        </TagInput>
      </Field>
      <Field>
        <FieldLabel htmlFor="read-only-regions">Regions</FieldLabel>
        <TagInput readOnly defaultValue={["us-east-1", "eu-west-1"]}>
          <TagInputList />
          <TagInputInput id="read-only-regions" />
        </TagInput>
      </Field>
    </div>
  );
}
```

## Props

The tags keep their order and are strings. To add one from outside the input, control `value`.
To manage loading yourself instead of with `loadSuggestions`, change `suggestions` as people type,
and pass `filter={null}` to show them as given. `TagInputContent` takes `side`,
`sideOffset`, `align`, `alignOffset` and `anchor`.

| Part | Extends |
| --- | --- |
| `TagInput` | `<div>` |
| `TagInputList` | — |
| `TagInputItem` | `<span>` |
| `TagInputInput` | `<input>` |
| `TagInputContent` | [`Autocomplete.Popup`](https://base-ui.com/react/components/autocomplete#popup) |
| `TagInputSuggestions` | [`Autocomplete.List`](https://base-ui.com/react/components/autocomplete#list) |
| `TagInputSuggestion` | [`Autocomplete.Item`](https://base-ui.com/react/components/autocomplete#item) |
| `TagInputEmpty` | [`Autocomplete.Empty`](https://base-ui.com/react/components/autocomplete#empty) |
| `TagInputStatus` | [`Autocomplete.Status`](https://base-ui.com/react/components/autocomplete#status) |
| `TagInputClear` | [`Button`](https://ui.jarv.is/components/button.md) |

The tables leave out `className`, `style` and `render`. Parts built on Base UI take them as its [styling](https://base-ui.com/react/handbook/styling) and [composition](https://base-ui.com/react/handbook/composition) guides describe.

### TagInput

| Prop                  | Type                                                                                              | Description                                                                                                                                                                                                                                            |
| --------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `value?`              | `string[]`                                                                                        | The tags.                                                                                                                                                                                                                                              |
| `defaultValue?`       | `string[]`                                                                                        |                                                                                                                                                                                                                                                        |
| `onValueChange?`      | `((tags: string[]) => void)`                                                                      | Called with every tag after one is added, edited or removed.                                                                                                                                                                                           |
| `inputValue?`         | `string`                                                                                          | The text typed but not yet added as a tag.                                                                                                                                                                                                             |
| `defaultInputValue?`  | `string`                                                                                          |                                                                                                                                                                                                                                                        |
| `onInputValueChange?` | `((value: string) => void)`                                                                       | Called as the typed text changes.                                                                                                                                                                                                                      |
| `max?`                | `number`                                                                                          | The most tags to take.                                                                                                                                                                                                                                 |
| `delimiters?`         | `string[]`                                                                                        | Characters that add the typed text as a tag, and split pasted text into tags along with new lines and tabs. Enter always adds it. Default: `[","]`                                                                                                     |
| `allowDuplicates?`    | `boolean`                                                                                         | Whether to take a tag that's already there. Tags are compared ignoring case. Default: `false`                                                                                                                                                          |
| `allowCustom?`        | `boolean`                                                                                         | Whether people can add tags that aren't suggestions. When it's false, only `suggestions`, or results `loadSuggestions` has returned, are taken: typed text that matches one, ignoring case, adds it, and anything else is turned away. Default: `true` |
| `validate?`           | `((tag: string, tags: string[]) => boolean)`                                                      | Returns whether to take a tag, given it and the tags so far. Tags are trimmed, with runs of spaces collapsed, first.                                                                                                                                   |
| `onTagReject?`        | `((tag: string, reason: "invalid" \| "duplicate" \| "max" \| "unlisted") => void)`                | Called for each tag turned away, with why: `validate` returned false, it's a `duplicate`, there are `max` tags already, or it's `unlisted` among the suggestions when `allowCustom` is false.                                                          |
| `addOnBlur?`          | `boolean`                                                                                         | Whether to add the typed text as a tag when the input loses focus. Default: `false`                                                                                                                                                                    |
| `addOnTab?`           | `boolean`                                                                                         | Whether Tab adds the typed text as a tag as focus moves on. Default: `false`                                                                                                                                                                           |
| `editable?`           | `boolean`                                                                                         | Whether people can edit a tag, by double-clicking it or pressing Enter while it's highlighted. Default: `false`                                                                                                                                        |
| `disabled?`           | `boolean`                                                                                         | Default: `false`                                                                                                                                                                                                                                       |
| `readOnly?`           | `boolean`                                                                                         | Shows the tags without letting people add or remove them. Default: `false`                                                                                                                                                                             |
| `required?`           | `boolean`                                                                                         | Whether a form can't be submitted without a tag, which fails as `valueMissing`. Typed text that isn't a tag yet doesn't count. Default: `false`                                                                                                        |
| `name?`               | `string`                                                                                          | Submits each tag with a form under this name, in order. A `Field`'s `name` works too.                                                                                                                                                                  |
| `form?`               | `string`                                                                                          | The `id` of the form to submit the tags with, when the tag input isn't inside it.                                                                                                                                                                      |
| `size?`               | `"default" \| "lg"`                                                                               | Default: `"default"`                                                                                                                                                                                                                                   |
| `suggestions?`        | `string[]`                                                                                        | Tags to suggest as people type, shown in a `TagInputContent`. Tags already added are left out.                                                                                                                                                         |
| `suggestionLabel?`    | `((suggestion: string) => string)`                                                                | The text people's typing is matched against for each suggestion, when it's more than the tag itself, such as a person's name for their email address.                                                                                                  |
| `filter?`             | `((suggestion: string, query: string, label: (suggestion: string) => string) => boolean) \| null` | Returns whether a suggestion matches the typed text, trimmed, given `suggestionLabel` to read it with. It matches anywhere in the suggestion by default. Pass `null` to show every suggestion as given, such as results from a search.                 |
| `loadSuggestions?`    | `((query: string, options: { signal: AbortSignal; }) => Promise<string[]>)`                       | Loads suggestions for the typed text, such as from a search endpoint, in place of `suggestions`. It's called once people pause typing, with a signal that aborts when they type again, and its results are shown as given.                             |
| `loadDeps?`           | `React.DependencyList`                                                                            | What `loadSuggestions` reads besides the typed text, such as a selected category. Suggestions load again when any of them changes.                                                                                                                     |
| `loadDelay?`          | `number`                                                                                          | How long to wait after people stop typing before loading suggestions, in milliseconds. Default: `300`                                                                                                                                                  |
| `autoHighlight?`      | `boolean`                                                                                         | Whether the first matching suggestion is highlighted as people type, so Enter adds it instead of the typed text. It's on by default when `allowCustom` is false.                                                                                       |

### TagInputList

| Prop        | Type                                                | Description                                                                                                                |
| ----------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `children?` | `((tag: string, index: number) => React.ReactNode)` | Renders each tag, usually as a `TagInputItem`. Without it, each tag is a `TagInputItem`. Default: `() => <TagInputItem />` |

### TagInputItem

| Prop          | Type      | Description                                                    |
| ------------- | --------- | -------------------------------------------------------------- |
| `showRemove?` | `boolean` | Whether to show a button that removes the tag. Default: `true` |

### TagInputContent

| Prop            | Type                                                                                                                          | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `initialFocus?` | `boolean \| React.RefObject<HTMLElement \| null> \| ((openType: InteractionType) => void \| boolean \| HTMLElement \| null)`  | Determines the element to focus when the popup is opened. - `false`: Do not move focus. - `true`: Move focus based on the default behavior (first tabbable element or popup). - `RefObject`: Move focus to the ref element. - `function`: Called with the interaction type (`mouse`, `touch`, `pen`, or `keyboard`). Return an element to focus, `true` to use the default behavior, or `false`/`undefined` to do nothing.                                                                                                                                                                                                                                        |
| `finalFocus?`   | `boolean \| React.RefObject<HTMLElement \| null> \| ((closeType: InteractionType) => void \| boolean \| HTMLElement \| null)` | Determines the element to focus when the popup is closed. - `false`: Do not move focus. - `true`: Move focus based on the default behavior (trigger or previously focused element). - `RefObject`: Move focus to the ref element. - `function`: Called with the interaction type (`mouse`, `touch`, `pen`, or `keyboard`). Return an element to focus, `true` to use the default behavior, or `false`/`undefined` to do nothing.                                                                                                                                                                                                                                  |
| `anchor?`       | `Element \| React.RefObject<Element \| null> \| VirtualElement \| (() => Element \| VirtualElement \| null) \| null`          | An element to position the popup against. By default, the popup will be positioned against the trigger.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `side?`         | `"bottom" \| "inline-end" \| "inline-start" \| "left" \| "right" \| "top"`                                                    | Which side of the anchor element to align the popup against. May automatically change to avoid collisions. Default: `"bottom"`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `sideOffset?`   | `number \| OffsetFunction`                                                                                                    | Distance between the anchor and the popup in pixels. Also accepts a function that returns the distance to read the dimensions of the anchor and positioner elements, along with its side and alignment. The function takes a `data` object parameter with the following properties: - `data.anchor`: the dimensions of the anchor element with properties `width` and `height`. - `data.positioner`: the dimensions of the positioner element with properties `width` and `height`. - `data.side`: which side of the anchor element the positioner is aligned against. - `data.align`: how the positioner is aligned relative to the specified side. Default: `4` |
| `align?`        | `"center" \| "end" \| "start"`                                                                                                | How to align the popup relative to the specified side. Default: `"start"`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `alignOffset?`  | `number \| OffsetFunction`                                                                                                    | Additional offset along the alignment axis in pixels. Also accepts a function that returns the offset to read the dimensions of the anchor and positioner elements, along with its side and alignment. The function takes a `data` object parameter with the following properties: - `data.anchor`: the dimensions of the anchor element with properties `width` and `height`. - `data.positioner`: the dimensions of the positioner element with properties `width` and `height`. - `data.side`: which side of the anchor element the positioner is aligned against. - `data.align`: how the positioner is aligned relative to the specified side. Default: `0`  |

### TagInputSuggestions

| Prop        | Type                                                 | Description                                                                                                                                                                                          |
| ----------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `children?` | `((item: string, index: number) => React.ReactNode)` | Renders each suggestion that matches, usually as a `TagInputSuggestion`. Without it, each one is a `TagInputSuggestion`. Default: `(item: string) => <TagInputSuggestion key={item} value={item} />` |

### TagInputSuggestion

| Prop            | Type                                                                           | Description                                                                                                                                                                                                                         |
| --------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `nativeButton?` | `boolean`                                                                      | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `true` if the rendered element is a native button. Default: `false`                                                       |
| `children?`     | `React.ReactNode`                                                              |                                                                                                                                                                                                                                     |
| `onClick?`      | `((event: BaseUIEvent<React.MouseEvent<HTMLDivElement, MouseEvent>>) => void)` | An optional click handler for the item when selected. It fires when clicking the item with the pointer, as well as when pressing `Enter` with the keyboard if the item is highlighted when the `Input` or `List` element has focus. |
| `index?`        | `number`                                                                       | The index of the item in the list. Improves performance when specified by avoiding the need to calculate the index automatically from the DOM.                                                                                      |
| `disabled?`     | `boolean`                                                                      | Whether the component should ignore user interaction. Default: `false`                                                                                                                                                              |
| `value`         | `string`                                                                       | The tag it adds.                                                                                                                                                                                                                    |

### TagInputStatus

| Prop        | Type                                                                                     | Description                                                                                                                         |
| ----------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `children?` | `((state: { loading: boolean; error: boolean; }) => React.ReactNode) \| React.ReactNode` | What to show while suggestions load or after they fail to. Without it, a spinner with "Searching…", or "Couldn't load suggestions." |

### TagInputClear

| Prop                     | Type                                                                                 | Description                                                                                                                                                                                       |
| ------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `focusableWhenDisabled?` | `boolean`                                                                            | Whether the button should be focusable when disabled. Default: `false`                                                                                                                            |
| `nativeButton?`          | `boolean`                                                                            | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). Default: `true` |
| `variant?`               | `"destructive" \| "ghost" \| "link" \| "outline" \| "primary" \| "secondary"`        | Default: `"ghost"`                                                                                                                                                                                |
| `size?`                  | `"default" \| "icon" \| "icon-lg" \| "icon-sm" \| "icon-xs" \| "lg" \| "sm" \| "xs"` | Default: `"icon-xs"`                                                                                                                                                                              |
