# TagInput

> Chips inside an input box. Enter or comma adds, Backspace removes the last, paste splits on commas and new lines; supports maxTags, validation, suggestions, and Field.

Source: https://docs.nasaqui.com/components/tag-input

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/tag-input.json
```

A text input that turns what you type into removable chips. Use it for free-form lists: labels, keywords,
email recipients. It is a Field control, so `FieldLabel`, `FieldDescription` and `FieldError` wire up as they do
for `Input`.

## When to use

- The user types their own values and there can be several.
- The values can also come from a suggestion list, but free entry is allowed.

## When not to use

- Picking from a fixed list: use [`Combobox`](https://docs.nasaqui.com/components/combobox) or [`Select`](https://docs.nasaqui.com/components/select).
- A single filter chosen from a few: use [`ChipGroup`](https://docs.nasaqui.com/components/chip-group).

## Import

```tsx
import { TagInput } from "@fadymondy/nasaq/web";
```

## Quick start

```tsx
import { Field, FieldLabel, TagInput } from "@fadymondy/nasaq/web";

export function Labels() {
  return (
    <Field>
      <FieldLabel>Labels</FieldLabel>
      <TagInput defaultValue={["design", "urgent"]} />
    </Field>
  );
}
```

## Anatomy

```
TagInput                     data-slot="tag-input"  (data-invalid, data-disabled)
├─ box                       data-slot="tag-input-box"
│  ├─ Badge + remove button  data-slot="tag-input-tag"
│  └─ input                  data-slot="tag-input-field"
├─ ul role=listbox           data-slot="tag-input-suggestions"  (only with suggestions)
└─ p role=alert              data-slot="tag-input-error"
```

## API

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `readonly string[]` | none | Controlled tags. |
| `defaultValue` | `readonly string[]` | `[]` | Initial tags when uncontrolled. |
| `onValueChange` | `(tags: string[]) => void` | none | |
| `maxTags` | `number` | none | At the limit new tags are refused with a message. |
| `validate` | `(tag: string, tags: readonly string[]) => boolean \| string` | none | `true` accepts, `false` shows a generic message, a string is the message to show. |
| `onReject` | `(tag: string, reason: "duplicate" \| "invalid" \| "max") => void` | none | |
| `suggestions` | `readonly string[]` | none | Filtered with `normalizeForSearch`; chosen tags are hidden. |
| `separators` | `readonly string[]` | `["Enter", ",", "،"]` | Keys that add the typed text. |
| `addOnBlur` | `boolean` | `true` | Add the pending text when the input loses focus. |
| `placeholder` | `string` | "Type and press Enter" / "اكتب ثم اضغط Enter" | Shown while there are no tags. |
| `disabled` | `boolean` | `false` | |
| `name` | `string` | none | Renders one hidden input per tag for native forms. |
| `invalid` | `boolean` | `false` | Danger border. Automatic inside `<Field invalid>`. |
| `inputProps` | Base UI Input props | none | For the text input: `id`, `aria-label`, `dir`. |

Duplicates are ignored, compared with `normalizeForSearch` (case and Arabic letter variants folded).

## Examples

Emails with a limit:

```tsx
import { Field, FieldLabel, TagInput } from "@fadymondy/nasaq/web";
import { useState } from "react";

export function Invite() {
  const [emails, setEmails] = useState<string[]>([]);
  return (
    <Field>
      <FieldLabel>Invite by email</FieldLabel>
      <TagInput
        value={emails}
        onValueChange={setEmails}
        maxTags={3}
        inputProps={{ dir: "ltr" }}
        validate={(tag) => /^\S+@\S+\.\S+$/.test(tag) || `${tag} is not a valid email.`}
      />
    </Field>
  );
}
```

Arabic with suggestions:

```tsx
import { Field, FieldLabel, TagInput } from "@fadymondy/nasaq/web";

export function Skills() {
  return (
    <Field>
      <FieldLabel>المهارات</FieldLabel>
      <TagInput suggestions={["إدارة المشاريع", "تصميم", "برمجة"]} />
    </Field>
  );
}
```

## Accessibility

| Key | Action |
| --- | --- |
| Enter, comma, Arabic comma | Adds the typed text as a tag. |
| Backspace (empty input) | Removes the last tag. |
| Arrow Down / Up | Moves through suggestions. |
| Enter (suggestion active) | Adds that suggestion. |
| Escape | Closes suggestions. |
| Tab | Moves to a tag's remove button, then out. |

- With `suggestions` the input is a `role="combobox"` with `aria-activedescendant`.
- Each remove button is named "Remove <tag>". Adding and removing are announced through a polite live region; refusals through `role="alert"`.
- Localise messages you return from `validate`; built-in ones follow the Nasaq locale.

## RTL & i18n

- Chips flow from the inline start. Tag text is wrapped in `<bdi>` so an English tag inside Arabic keeps its order.
- The Arabic comma is a separator by default.
- Pass `inputProps={{ dir: "ltr" }}` for emails and URLs.

## Styling & tokens

Box uses `border-input`, `bg-card`, `border-nq-focus`; errors use `border-nq-danger` and `text-nq-danger-text`. Target `[data-invalid]` and `[data-disabled]` on the root. Extend with `className`; do not override colours with hex.

## Do / Don't

- Do set `maxTags` when the backend has a limit.
- Do validate on the server too.
- Do not use it for a fixed choice list; use Combobox.

## Related

- [Field](https://docs.nasaqui.com/components/field)
- [Badge](https://docs.nasaqui.com/components/badge)
- [ChipGroup](https://docs.nasaqui.com/components/chip-group)

## Lab

https://docs.nasaqui.com/?path=/docs/components-forms-tag-input--docs

## Code

### React

```tsx
import { Field, FieldLabel, TagInput } from "@fadymondy/nasaq/web";

export function Labels() {
  return (
    <Field>
      <FieldLabel>Labels</FieldLabel>
      <TagInput defaultValue={["design", "urgent"]} />
    </Field>
  );
}
```

### shadcn

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

export function Labels() {
  return (
    <Field>
      <FieldLabel>Labels</FieldLabel>
      <TagInput defaultValue={["design", "urgent"]} />
    </Field>
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { NqField, NqFieldLabel, NqTagInput } from "@fadymondy/nasaq/vue";
</script>

<template>
  <NqField>
    <NqFieldLabel>Labels</NqFieldLabel>
    <NqTagInput :default-value="['design', 'urgent']" />
  </NqField>
</template>
```

### Blade

```blade
<div class="w-80">
    <x-nq::tag-input name="labels" :value="['design', 'urgent']" :suggestions="['design', 'urgent', 'research', 'review']" :max-tags="5" aria-label="Labels" />
</div>
```

### HTML + Alpine

```html
<div class="w-80">
    <div data-slot="tag-input" x-data="nqTagInput(JSON.parse('[\u0022design\u0022,\u0022urgent\u0022]'), JSON.parse('{\u0022maxTags\u0022:5,\u0022suggestions\u0022:[\u0022design\u0022,\u0022urgent\u0022,\u0022research\u0022,\u0022review\u0022]}'))" x-modelable="tags" x-bind="root"
         class="relative w-full">
    <div data-slot="tag-input-box" x-bind="box"
        class="flex w-full min-w-0 flex-wrap items-center gap-1.5 rounded-control border border-input bg-card px-2 py-1 text-body text-foreground min-h-[max(var(--nq-control),var(--nq-touch-min,0px))] transition-colors duration-150 ease-nq focus-within:border-nq-focus focus-within:outline-1 focus-within:outline-nq-focus has-[[data-invalid]]:border-nq-danger has-[[aria-invalid=true]]:border-nq-danger">
        <template x-for="(tag, i) in tags" :key="tag">
            <span data-slot="tag-input-tag" data-variant="neutral" class="inline-flex w-fit shrink-0 items-center justify-center whitespace-nowrap rounded-control border border-transparent bg-muted px-2 py-0.5 font-medium text-foreground h-6 gap-0.5 ps-2 pe-0.5 text-body-sm">
                <bdi x-text="tag"></bdi>
                <button type="button"  :aria-label="'Remove' + ' ' + tag" @click.stop="removeAt(i)"
                    class="inline-flex size-5 items-center justify-center rounded-[3px] text-muted-foreground outline-none hover:bg-nq-hover hover:text-foreground focus-visible:outline-2 focus-visible:outline-nq-focus">
                    <span aria-hidden="true" class="flex [&_svg]:size-3"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M18 6 6 18"/>
  <path d="m6 6 12 12"/>
</svg></span>
                </button>
            </span>
        </template>
        <input data-slot="tag-input-field" x-ref="field" x-bind="field" autocomplete="off"             :placeholder="tags.length ? undefined : 'Type and press Enter'"
             role="combobox" aria-autocomplete="list" :aria-controls="listId()"             aria-label="Labels" class="h-7 min-w-24 flex-1 border-0 bg-transparent px-1 text-body outline-none placeholder:text-muted-foreground pointer-coarse:text-[16px]">
    </div>
            <ul x-show="showList()" x-cloak style="display: none" :id="listId()" role="listbox" aria-label="Suggestions" data-slot="tag-input-suggestions"
            class="absolute inset-x-0 top-full z-50 mt-1 max-h-56 overflow-y-auto rounded-floating border border-border bg-popover p-1 text-popover-foreground">
            <template x-for="(s, i) in filtered()" :key="s">
                <li role="option" data-slot="tag-input-suggestion" x-bind="option(s, i)" class="cursor-pointer rounded-control px-2 py-1.5 text-body-sm data-active:bg-nq-hover hover:bg-nq-hover">
                    <bdi x-text="s"></bdi>
                </li>
            </template>
        </ul>
        <p x-show="error" x-cloak style="display: none" role="alert" data-slot="tag-input-error" x-text="error" class="mt-1.5 text-caption text-nq-danger-text"></p>
    <span role="status" aria-live="polite" class="sr-only" x-text="message"></span>
            <template x-for="tag in tags" :key="tag"><input type="hidden" name="labels" :value="tag"></template>
    </div>
</div>
```
