# Tag input

Package: @commenda/pathfinder@0.1.0
Page: /docs/components/tag-input
Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=179-3

```tsx
import { TagInput } from '@commenda/pathfinder/tag-input';

export const TagInputDefault = () => (
  <TagInput
    label="Labels"
    defaultValue={['Finance', 'Reviewed']}
    description="Press Enter or comma to add a label."
  />
);
```


## Usage [#usage]

Import `TagInput` from `@commenda/pathfinder/tag-input`. Import the compiled stylesheet once. Use Tag input for free-form values; use Multi select when values come from a known set of options.

## Examples [#examples]

### Editing and paste [#editing-and-paste]


```tsx
import { useState } from 'react';

import { TagInput } from '@commenda/pathfinder/tag-input';

export const TagInputEditing = () => {
  const [tags, setTags] = useState<readonly string[]>([
    'Finance',
    'Operations',
  ]);

  return (
    <TagInput
      label="Team labels"
      value={tags}
      onValueChange={setTags}
      entryConfig={{ editable: true, addOnPaste: true, maxTags: 5 }}
      description="Add up to five labels. Double-click a tag to edit it, or paste comma-separated values."
    />
  );
};
```


### Validation [#validation]


```tsx
import { TagInput } from '@commenda/pathfinder/tag-input';

export const TagInputValidation = () => (
  <TagInput
    label="Recipients"
    entryConfig={{
      addOnPaste: true,
      validate: (value) =>
        /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
          ? undefined
          : 'Enter an email address.',
    }}
    description="This example checks email syntax only; the application owns recipient validation."
  />
);
```


### States [#states]


```tsx
import { TagInput } from '@commenda/pathfinder/tag-input';

export const TagInputStates = () => (
  <div style={{ display: 'grid', gap: 24, width: '100%' }}>
    <TagInput label="Read-only labels" defaultValue={['Finance']} readOnly />
    <TagInput
      label="Protected labels"
      defaultValue={['Archived']}
      disableConfig={{ isDisabled: true, reason: 'This record is archived.' }}
    />
    <TagInput
      label="Required labels"
      required
      errorConfig={{
        message: 'Add a label before continuing.',
        reveal: 'immediate',
      }}
    />
  </div>
);
```


## API reference [#api-reference]

| Prop | Type | Requirement | Behavior |
| --- | --- | --- | --- |
| `label` | `string` | conditional | Required accessible field name; alternatively use `aria-label` |
| `hideLabel` | `boolean` | optional | Required accessible field name; alternatively use `aria-label` |
| `value` | `readonly string[]` | conditional | Controlled values; callback is required with `value` |
| `onValueChange` | `((value: readonly string[]) => void)` | conditional | Controlled values; callback is required with `value` |
| `defaultValue` | `readonly string[]` | optional | Initial uncontrolled values |
| `entryConfig` | `{ editable?: boolean; addOnPaste?: boolean; addOnTab?: boolean; delimiter?: string; maxTags?: number; validate?: (tag: string) => string \| undefined; }` | optional | `editable`, `addOnPaste`, `addOnTab`, `delimiter`, `maxTags`, `validate` |
| `name` | `string` | optional | Submit each tag as a repeated named value; read with `FormData.getAll(name)` |
| `form` | `string` | optional | Submit each tag as a repeated named value; read with `FormData.getAll(name)` |
| `readOnly` | `boolean` | optional | Values submit, editing and removal are blocked |
| `disableConfig` | `DisableConfig` | optional | Blocks editing and omits values from form submission |
| `description` | `string` | optional | Associated guidance and validation |
| `errorConfig` | `{ message: string; reveal?: "after-blur" \| "immediate"; }` | optional | Associated guidance and validation |
| `required` | `boolean` | conditional | Associated guidance and validation |
| `placeholder` | `string` | optional | The ref targets the entry input |
| `onBlur` | `FocusEventHandler<HTMLInputElement>` | optional | The ref targets the entry input |
| `aria-label` | `string` | conditional | Accessible name when the field has no visible label. |
| `labelConfig` | `{ optionalLabel: string; help?: never; } \| { help: string; optionalLabel?: never; }` | optional | Choose either `optionalLabel` or `help`, never both. `never` excludes the other field. |
| `id` | `string` | optional | Stable HTML identifier for the component. |
| `className` | `string` | optional | Additional classes for the component root. |

### entryConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `editable` | `boolean` | No |  |
| `addOnPaste` | `boolean` | No |  |
| `addOnTab` | `boolean` | No |  |
| `delimiter` | `string` | No |  |
| `maxTags` | `number` | No |  |
| `validate` | `((tag: string) => string \| undefined)` | No | Return a message to reject a tag; undefined accepts it. |

### disableConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `isDisabled` | `boolean` | Yes |  |
| `reason` | `string` | No |  |

### errorConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `message` | `string` | Yes |  |
| `reveal` | `"after-blur" \| "immediate"` | No |  |

### labelConfig fields

Choose one shape; excluded fields cannot be combined.

#### Option 1

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `optionalLabel` | `string` | Yes |  |

Do not supply: `help`.

#### Option 2

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `help` | `string` | Yes |  |

Do not supply: `optionalLabel`.

## Keyboard and validation [#keyboard-and-validation]

Enter or the configured delimiter adds a trimmed tag. Duplicates are rejected. Arrow keys navigate tags, Backspace removes a highlighted tag, and editable tags support keyboard editing. Tab retains normal focus navigation unless `addOnTab` is enabled. Composition input does not create a tag until composition is complete.

Provide unique, nonempty initial values. Validation belongs to the application; the component never treats an email-shaped value as proof of an address or identity. Uncontrolled native resets restore defaults. Controlled resets remain application-owned.

## Design rules [#design-rules]

- **TAG-INPUT-001:** Reuse the Dice UI primitive used by Kakashi TagInput; ship Pathfinder’s compiled styling without a runtime Kakashi dependency.
- **TAG-INPUT-002:** Use the existing field shell and Tag geometry. Wrap long tags without pushing the entry field outside its container.
- **TAG-INPUT-003:** Announce invalid entries and retain keyboard focus during editing and removal.
- **TAG-INPUT-004:** Read-only and disabled modes prevent mutations; only enabled values participate in form submission.

Tag input is a Kakashi-derived composition of Input and Tag, not a newly extracted standalone Figma component.
