# TagsInput (/docs/tags-input)





`TagsInput` collects free-form tags. Unlike [`MultiSelect`](/docs/multi-select) there's no fixed option list — the user **types and presses Enter** (or a delimiter) to mint a tag. It's built on Ark UI's dedicated TagsInput machine, and each tag renders as a [`Pill`](/docs/pill)-styled chip with a delete button.

```tsx
import { TagsInput } from '@42-central/ui-react/tags-input';

<TagsInput defaultValue={['c', 'unix']} placeholder="Add a skill…" />
```

## Playground [#playground]

<story.Playground />

## Adding & removing [#adding--removing]

Type and press **Enter** or a **delimiter** (`,` by default — set `splitChars`) to add a tag. Pasting adds tags too (`addOnPaste&#x60;). Remove with a tag's &#x2A;*×**, **Backspace** on the empty field, or the **clear** button (`clearable`). Tags are **editable** — double-click one to edit it in place.

## Limits & duplicates [#limits--duplicates]

`maxTags` caps the count; `allowDuplicates` lets the same tag appear more than once (off by default).

<story.Preview variant="Max tags" />

## Max lines [#max-lines]

`maxLines&#x60; caps the visible tags at N rows; any beyond collapse into a static **+N** counter. The hidden tags stay in the value and submit normally — there's no inline reveal, so reach for this when the field would otherwise grow unbounded.

<story.Preview variant="Max lines" />

## Start & end slots [#start--end-slots]

`startSlot` and `endSlot` place content at the field's inline-start / inline-end with the same slot layout as [`Input`](/docs/input). `endSlot` renders before the clear button.

<Preview align="block" code="&#x22;import { Hash } from 'lucide-react';\n\n<TagsInput\n  defaultValue={['c', 'unix']}\n  startSlot={<Hash />}\n  placeholder=\&#x22;Add a skill…\&#x22;\n/>&#x22;">
  <div className="w-full max-w-[22rem]">
    <TagsInput defaultValue="['c', 'unix']" startSlot="<Hash />" placeholder="Add a skill…" />
  </div>
</Preview>

## Testing tags [#testing-tags]

`testId` lands on the real typing `<input>`. Each rendered tag additionally gets its own
`` `${testId}-tag-${index}` `` (0-indexed by rendered position, since tags are freeform strings
with no stable id of their own) — see the [Testing](/docs/testing) guide.

```tsx
<TagsInput testId="skills-input" defaultValue={['c', 'unix']} />
// → input: data-testid="skills-input"
// → tags: data-testid="skills-input-tag-0", "skills-input-tag-1"
```

## Controlled [#controlled]

`value` is a `string[]`; `onChange` fires with the full list on every add, remove, or edit.

```tsx
'use client';
import { useState } from 'react';
import { TagsInput } from '@42-central/ui-react/tags-input';

export function Keywords() {
  const [value, setValue] = useState<string[]>([]);
  return (
    <TagsInput
      value={value}
      onChange={setValue}
      placeholder="Add keywords"
      clearable
    />
  );
}
```

## Label, description & error [#label-description--error]

Built-in via [`Field`](/docs/field):

<Preview align="block" code="&#x22;<TagsInput\n  label=\&#x22;Skills\&#x22;\n  description=\&#x22;Press Enter to add\&#x22;\n  defaultValue={['c']}\n/>&#x22;">
  <div className="w-full max-w-[22rem]">
    <TagsInput label="Skills" description="Press Enter to add" defaultValue="['c', 'unix']" placeholder="Add a skill…" />
  </div>
</Preview>

## API [#api]

<TypeTable
  type="{
  value: { type: 'string[]', description: 'Controlled tags.' },
  defaultValue: { type: 'string[]', description: 'Uncontrolled initial tags.' },
  onChange: { type: '(value: string[]) => void', description: 'Fires with the full tag list.' },
  placeholder: { type: 'string', description: 'Placeholder shown while the field is empty.' },
  clearable: { type: 'boolean', default: 'false', description: 'Show a clear-all button.' },
  maxTags: { type: 'number', description: 'Cap the number of tags.' },
  maxLines: { type: 'number', description: 'Cap the tags at N rows; the rest collapse into a static &#x22;+N&#x22; counter.' },
  startSlot: { type: 'ReactNode', description: 'Inline-start adornment (icon, label).' },
  endSlot: { type: 'ReactNode', description: 'Inline-end adornment — before the clear button.' },
  allowDuplicates: { type: 'boolean', default: 'false', description: 'Allow the same tag more than once.' },
  splitChars: { type: 'string | RegExp', default: '&#x22;,&#x22;', description: 'Delimiter that splits typed/pasted text into tags.' },
  addOnPaste: { type: 'boolean', description: 'Create tags from pasted text.' },
  size: { type: '&#x22;xs&#x22; | &#x22;sm&#x22; | &#x22;md&#x22; | &#x22;lg&#x22; | &#x22;xl&#x22;', default: '&#x22;md&#x22;', description: 'Control / tag sizing.' },
  variant: { type: '&#x22;default&#x22; | &#x22;unstyled&#x22;', default: '&#x22;default&#x22;', description: 'Control shell treatment.' },
  color: { type: 'Color', default: '&#x22;brand&#x22;', description: 'Focus-ring accent.' },
  label: { type: 'ReactNode', description: 'Field label.' },
  description: { type: 'ReactNode', description: 'Field helper text.' },
  error: { type: 'ReactNode', description: 'Field error; sets the invalid state.' },
  required: { type: 'boolean', default: 'false', description: 'Required flag + asterisk.' },
  disabled: { type: 'boolean', default: 'false', description: 'Disable the control.' },
  readOnly: { type: 'boolean', default: 'false', description: 'Read-only (no add / remove / edit).' },
  name: { type: 'string', description: 'Name for the hidden inputs (form submission).' },
  translations: {
    type: 'TagsInputTranslations',
    description: &#x22;Ark's own TagsInput wording, forwarded as-is: clearTriggerLabel, deleteTagTriggerLabel(tag), inputLabel(count), and the screen-reader announcements (tagAdded, tagDeleted, tagsPasted, …). Every key is optional and falls back to Ark's English default.&#x22;,
  },
  classNames: { type: '{ field?, control?, input?, tag?, tagRemove? }', description: 'Per-part class overrides.' },
  testId: { type: 'string', description: 'data-testid on the typing <input>. Each rendered tag additionally gets `${testId}-tag-${index}`. See the Testing guide.' },
}"
/>

## Accessibility [#accessibility]

* Full TagsInput keyboard model from Ark: `Enter` / delimiter adds, `Backspace` removes the last tag from the empty field, `←` / `→` move between tags, `Enter` on a tag edits it, `Esc` cancels an edit.
* Each tag's delete is a labelled button; the field announces additions, deletions, and edits; label / description / error are associated via the built-in `Field`.
* A hidden input backs form submission; pass `name`.
* RTL: tags, field, and clear button mirror under `dir="rtl"`.
