<!-- Protocore Design System — TagsInput -->
# TagsInput

- **Category:** Inputs (`inputs`)
- **Slug:** `inputs/tags-input`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { TagsInput } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/inputs/tags-input

> A token / multi-value field: typed values commit into dismissible Tags inside a sunken input container, with dedupe, max, and validation.

## When to use it

Use **TagsInput** to collect an open-ended set of short freeform values — labels, email recipients, keywords — where the options aren't known ahead of time. When users pick from a **known** list, use a multi-select **Combobox** or a **FilterBar**. The committed values render as static **Tag**s, matching the metadata vocabulary used elsewhere.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `addOnKeys` | `string[]` | no | `["Enter", ","]` | Keys that commit the current draft as a value. Default `["Enter", ","]`. |
| `allowDuplicates` | `boolean` | no | `false` | Allow the same value more than once. Default `false` (deduped). |
| `aria-label` | `string` | no | — | Accessible label for the text entry. |
| `className` | `string` | no | — | — |
| `defaultValue` | `string[]` | no | — | Initial list of values when uncontrolled. |
| `disabled` | `boolean` | no | `false` | Disable adding and removing values. |
| `invalid` | `boolean` | no | `false` | Mark the field invalid — danger border plus `aria-invalid`. |
| `max` | `number` | no | — | Maximum number of values. Further additions are ignored once reached. |
| `onValueChange` | `((value: string[]) => void)` | no | — | Fires with the next list whenever a value is added or removed. |
| `placeholder` | `string` | no | — | Placeholder for the text entry (shown only while empty). |
| `size` | `enum` | no | `md` | Control height: `sm` (32) · `md` (36, default) · `lg` (40) — the minimum row height. |
| `style` | `CSSProperties` | no | — | — |
| `validate` | `((value: string, current: string[]) => boolean)` | no | — | Return `false` to reject a candidate before it is added. |
| `value` | `string[]` | no | — | Controlled list of values. Pair with `onValueChange`. |

## Examples

### Basics

Type a value and press Enter (or comma) to commit it as a Tag; Backspace on an empty entry removes the last one. Controllable via `value` / `defaultValue` (`string[]`) + `onValueChange`.

```tsx
import { useState } from "react";
import { TagsInput, Text } from "@protocore/pds";

export default function Demo() {
  const [tags, setTags] = useState<string[]>(["frankfurt", "prod"]);

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 8, maxWidth: 380 }}>
      <TagsInput
        value={tags}
        onValueChange={setTags}
        placeholder="Add a tag, press Enter…"
        aria-label="Deployment tags"
      />
      <Text as="span" size="sm" mono color="muted">
        {tags.length} tag{tags.length === 1 ? "" : "s"}
      </Text>
    </div>
  );
}
```

### Validated, capped

Pass a `validate` predicate to reject malformed values and `max` to cap the count — the entry disables once the cap is hit. Rejections are announced through a polite live region.

```tsx
import { useState } from "react";
import { Field, TagsInput } from "@protocore/pds";

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

export default function Demo() {
  const [recipients, setRecipients] = useState<string[]>(["ops@protocore.io"]);

  return (
    <div style={{ maxWidth: 420 }}>
      <Field label="Notify on deploy" hint="Up to 5 addresses. Press Enter or comma to add.">
        <TagsInput
          value={recipients}
          onValueChange={setRecipients}
          max={5}
          validate={(v) => EMAIL.test(v)}
          placeholder="name@company.com"
          aria-label="Recipient emails"
        />
      </Field>
    </div>
  );
}
```

## Do & don't

**Do**

- Label the field via a `Field` label or `aria-label` on the entry.
- Use `validate` to keep out malformed values instead of accepting anything.
- Set `max` when there's a real upper bound so the UI can stop accepting input.
- Keep values short — they render as mono metadata tags, not sentences.

**Don't**

- Don't use TagsInput to pick from a fixed option set — reach for Combobox.
- Don't allow duplicates unless order/repetition carries meaning.
- Don't hide the removal affordance; every tag keeps its × button.
- Don't stuff paragraphs into a tag — that's what Textarea is for.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `Enter / ,` | Commit the current draft as a tag (configurable via `addOnKeys`). |
| `Backspace` | When the entry is empty, remove the last tag. |
| `Tab` | Move to the entry; the entry is the single tab stop for adding values. |

**Notes**

- The text entry is a `role="combobox"` labelled by `aria-label`; each tag has a `Remove <value>` button.
- Adds, removals, duplicates, and rejections are announced via a visually-hidden `aria-live="polite"` region.
- `invalid` sets `aria-invalid` on the entry and a danger border on the container.

## Related

`input`, `field`, `combobox`, `chip`, `tag`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
