# TagInput

> Type and press Enter to add removable tags. Submits as repeated hidden inputs.

URL: https://apogee.avaui.dev/docs/tag-input
Last updated: 2026-10-15

Type and press Enter to add removable tags. Submits as repeated hidden inputs.

Last updated October 15, 2026

Uses JavaScript

Lets you type a tag and press Enter or comma to add it as a removable chip, and Backspace on an empty field to remove the last one. Each tag submits as a repeated `name` hidden input; JS un-names the free-text field so it stops double-submitting once it’s just for typing.

**Without JS:** Initial tags render as plain, non-removable chips (their remove buttons are hidden) and still submit via their hidden inputs. The free-text field keeps its \`name\`, so whatever's typed but not yet added also submits alongside the tags.

```astro
<TagInput name="skills" value={['Astro', 'Tailwind']} placeholder="Add a skill…" />
```

## Add

```sh
npx @ava-ui/apogee add tag-input
```

No Apogee component dependencies. Depends on `@lucide/astro` (renders the remove icon). Ships one scoped JS island (progressive enhancement).

## Usage

```astro
---
import TagInput from '@/components/apogee/TagInput/TagInput.astro'
---
<TagInput name="skills" value={['Astro', 'Tailwind']} />
```

## API

`@composable` — exposed to Content Editors in AVA. Any other attribute passes through to the root `<div>` (not the free-text `<input>`).

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string` | — | Field name each tag submits under, as a repeated hidden input. |
| `value` | `string[]` | `[]` | Initial tags, rendered as removable chips. |
| `placeholder` | `string` | `'Add tag…'` | Placeholder on the free-text field. |
| `variant` | `'outline' \| 'faded' \| 'ghost' \| 'headless'` | `'outline'` | The box style, shared with every text field. |
| `hasError` | `boolean` | `false` | Marks the field invalid (`aria-invalid`, critical border). |
| `id` | `string` | generated | Id of the typing field; the label points at it. |
| `label` | `string` | — | Label, through Field. |
| `hint` | `string` | — | Helper text under the box. |
| `error` | `string` | — | Error message; marks the field invalid. |
| `required` | `boolean` | `false` | Native `required` on the typing field while there are no tags. |
| `size` | `'small' \| 'medium' \| 'large' \| 'xlarge'` | `'medium'` | Minimum height and text size, matching the other fields. |
| `maxTags` | `number` | — | Stops accepting tags at this count; the field reads “Limit reached” until one is removed. |
| `allowDuplicates` | `boolean` | `false` | Accept a tag that already exists (the check is case-insensitive). |
| `class` | `string` | — | Extra classes on the Field root, appended last. |

## Label, sizes and limits

```astro
<TagInput name="topics" label="Topics" hint="Up to two." maxTags={2} value={['design']} />
<TagInput name="aliases" label="Aliases" allowDuplicates size="small" />
<TagInput name="emails" label="Invite" size="large" placeholder="name@company.com" />
```

## States

-   **Empty vs. seeded** — start with `value` for pre-set tags, or omit it for an empty field.
-   **Focus-within** — the whole container gets a visible ring when the free-text field has focus.
-   **Hover** on a chip’s remove button — the icon darkens from `--foreground-neutral-faded` to `--foreground-neutral`.
-   **No JS** — remove buttons on the initial tags are hidden (`no-js:hidden`); the free-text field keeps its `name`, so typed-but-not-yet-added text still submits alongside the tags.

```astro
<TagInput name="tags" value={['Design', 'Engineering', 'Marketing']} />
```

## Examples

Empty, ready for input:

```astro
<TagInput name="skills" />
```

Custom placeholder:

```astro
<TagInput name="topics" placeholder="Add a topic and press Enter…" />
```

Each tag submits as a separate `name` entry — reading it server-side yields an array:

```js
const skills = formData.getAll('skills') // ['Astro', 'Tailwind', …]
```

## Events

The root dispatches `apogee:tag-input-change` (`detail: { values: string[] }`, bubbles) after every add or remove. Duplicate labels are ignored.

```astro
<TagInput name="tags" id="tags" value={['astro']} />
<output for="tags">astro</output>
<script>
  document.querySelector('.apogee-taginput')?.addEventListener('apogee:tag-input-change', (e) => {
    document.querySelector('output').textContent = e.detail.values.join(', ')
  })
</script>
```

## Accessibility

| Key | Action |
| --- | --- |
| `Enter` `,` | Add the typed text as a tag. |
| `Backspace` | In an empty field, removes the last tag. |
| `Tab` | Moves between the text field and each remove button. |
| `Enter` `Space` | On a remove button, remove that tag; focus moves to the next remove button, or the text field. |

-   The free-text field is a real `<input type="text">`; each tag’s remove control is a real `<button type="button" aria-label="Remove {tag}">` — both keyboard-reachable via `Tab` in source order.
-   The focus-within ring shows the whole control is active, not just the text field.
-   **No JS** — initial tags still submit (hidden inputs), and the free-text field keeps its `name`, so typed-but-not-yet-added text submits too; the remove buttons are hidden, so nothing can be removed without JS.

## Tokens

| Token | Used for |
| --- | --- |
| `--border-neutral` | Container border |
| `--background-page` | Container + field background |
| `--border-primary` | Focus border (focus-within) |
| `--background-neutral-faded` | Chip background |
| `--foreground-neutral` | Chip text + remove icon (hover) |
| `--foreground-neutral-faded` | Field placeholder + remove icon (default) |

## Do / Don’t

-   **Do** seed `value` with existing tags when editing — they submit even before JS finishes loading.
-   **Do** keep `placeholder` short and action-oriented (“Add a skill…”).
-   **Don’t** assume typed text is lost without JS — the free-text field keeps its `name` and submits as one entry, though it won’t be split into separate tags server-side unless you parse it yourself (Enter/comma splitting is JS-only).
-   **Don’t** use TagInput for a single value — that’s a plain text `<input>`.

## Content

-   Placeholder names what’s being added (“Add a skill…”), not “Type here”.
-   Tags are short, sentence case, no ending punctuation — same rules as `Tag`.

## Related

[

TagA chip for labels and categories. Optionally a link, optionally removable.

](https://apogee.avaui.dev/docs/tag)[

ComboboxA text input with a styled, filterable dropdown of suggestions, anchored under the input in the top layer.

](https://apogee.avaui.dev/docs/combobox)[

InputA labeled text field with hint/error messaging, wired for accessibility — plus a pure-CSS floating label.

](https://apogee.avaui.dev/docs/input)
