Skip to main content
Copy for LLM

Tags Input

The <x-wirekit::tags-input> component allows free-form tag creation. Users type text and press Enter or comma to create a tag chip. Each chip has a remove button, and the combined values are submitted as an array.

Basic Usage

Basic Tags Input

With Preexisting Tags

Preset Tags

With Max Tags

Maximum 3 Tags

You can add up to 3 keywords.

When the maximum is reached the text field turns read-only and carries aria-disabled="true", and a screen-reader announcement confirms the limit. It keeps its place in the tab order deliberately — a field that disappears from the keyboard the moment it fills up leaves a reader arriving at nothing, with no explanation of what changed.

Error State

Tags input with an error message

At least one tag is required.

Width

Like all WireKit form components, the tags input fills its container (w-full). Control the width via the parent element:

<div class="max-w-md">
    <x-wirekit::tags-input label="Skills" name="skills" />
</div>

See Input — Width for more layout examples (grid columns, mixed widths).

Typing, committing, and getting the key back

The full key table is in Keyboard Interaction below; two behaviors around it are worth stating where a reader meets the component.

Escape only takes the key when there is text to clear. In an empty field it passes through, so a tags input inside a modal or a drawer does not swallow the key that closes it.

Duplicate tags are refused — the same tag cannot appear twice. The text you typed stays in the field and a screen-reader announcement says the tag is already in the list, so the refusal is not silent for a reader who cannot see the chips.

Form Submission

Each tag is submitted via a hidden <input type="hidden" name="name[]">, so Laravel receives a plain array of strings. name="tags" and name="tags[]" both submit tags[].

Livewire Binding

Bind the array of tags with wire:model on the component tag:

<x-wirekit::tags-input label="Labels" wire:model.live="labels" name="labels" :value="$labels" />

The set lives in Alpine and is exposed to the binding, so the property receives the full array whenever a tag is added or removed. Pass the current value with :value as well to seed what is already there.

In an Alpine component of your own, x-model on the tag binds the same array. See An x-model on a WireKit component.

Optimistic UI

Pass the name of the Livewire method the field should call and the set is sent the moment a tag is added or removed:

<x-wirekit::tags-input
    name="labels"
    label="Labels"
    :value="$labels"
    optimistic="saveLabels"
/>

Load wirekit-optimistic.js alongside whichever bundle you already use — below it, in your layout:

@wirekitScripts
<script src="{{ asset('vendor/wirekit/wirekit-optimistic.js') }}" defer></script>

Try it

The demo below runs the real path: the change shows immediately, the outline says it is provisional, and the server's answer either confirms it silently or takes it back.

Optimistic tags — accepted, refused, and a slow answer

The <livewire:demos.…> wrapper above exists only on this site — it supplies the demo methods so the page can show a real round trip. The block under it is what you write.

A refusal keeps the change rather than undoing it. A tag is typed, so undoing a failed add would delete what you just wrote. Removal takes the same exit for a structural reason: the set is one value, so a rollback restores the whole set — there is no way to undo a removal without also undoing an addition that happened alongside it. A refused removal therefore stays removed and says so, which is recoverable: the tag is one keystroke away.

The field is not marked invalid. aria-invalid means this value is wrong, and a save that failed on the network says nothing about the tags.

Props

Prop Type Default Description
required bool false Marks the field as required. Emits aria-required="true" on the role="group" wrapper and renders the label indicator (*). Deliberately NOT the native attribute on the composer input: with tags already added and the composer empty, that would block submission of a field that is in fact filled. What counts is the list: in a form, an empty required tags input stops the submit as a native required field does: the browser's own message shows under the field, in the reader's language, and the focus moves to the composer input when it is the first empty field.
label string|null null Label text above the input
hint string|null null Help text below the input
help string|null null An explanation of the field, shown in a tooltip from a question mark beside the label and read as the field's description. Needs a label to stand beside. See Help beside the label
error string|null null Error message (also reads from $errors)
name string|null null Form field name (submits as name[])
id string|null auto-generated The text field's id. A <label for> of yours with this id names the field, and then no fallback name stands over it
value array [] Initial tags
maxTags int|null null Maximum number of tags (null = unlimited)
placeholder string 'Add a tag…' Placeholder text in the text input
disabled bool false Disables the field and the buttons that remove a tag. Like a disabled native field, it is left out of the data a form submits
scope string|null null Scoped personalization key
optimistic string|null null Livewire method to call when a tag is added or removed, showing the new set before the server answers. A refusal keeps the change and says it was not saved — a rollback would delete text you typed. See Optimistic UI.
optimisticArgs array [] Extra arguments appended to the optimistic action call, after the new value — the row this control belongs to.
announceError bool|null null Render the error message in an aria-live="polite" region so a validation error that appears after render reaches a screen reader. Precedence: this prop, then the parent <x-wirekit::form :announce-errors>, then config('wirekit.a11y.announce_error'). Set false when the page runs its own live region.

Slots

Slot Description
labelAfter Markup after the label and its help question mark, in the same row and outside both, such as a badge that says a setting is not decided yet. See A Mark After the Label.

Accessibility

  • The text field carries autocomplete="off", so the browser's own suggestions do not open over the field where a tag is typed. An autocomplete of yours does not apply there.
  • An autofocus, inputmode, enterkeyhint, autocapitalize, spellcheck or maxlength written on the tag goes to the text field. There, maxlength limits one tag as it is typed.
  • When a label is given, the input area is a role="group" named by it, so a reader who lands on the fourth chip's remove button is told which field it belongs to. Without a label there is no group: an unnamed one adds a level to walk through and says nothing at the top of it
  • Each tag chip uses role="listitem" inside a role="list" container
  • Remove buttons have aria-label="Remove {tag}" for screen readers
  • Every change to the set is announced in a polite live region — a tag added, a tag removed (by the chip's own button or by Backspace), a duplicate refused, and the maximum reached. The two refusals are the ones that matter most: without them a reader has evidence that nothing happened and none of why
  • When maxTags is reached, the field becomes read-only, gets aria-disabled="true", and the limit is announced
  • aria-invalid="true" and aria-describedby set on error or hint
  • All chip remove icons are aria-hidden="true" — the button label provides the accessible name

Keyboard Interaction

Key Action
Tab Move focus to the tags input
Type characters Build the next tag
Enter / , (comma) Commit the typed text as a new tag
Backspace (in empty input) Remove the last tag
Escape Cancel the in-progress tag; passes through when the field is already empty

Targeted removal is by Tab, not by arrow keys: every chip ships its own remove button, so each tag is already a stop in the tab order with its own accessible name.

A comma ends a tag however it reaches the field: typed, pasted, dictated or from an on-screen keyboard. A pasted list such as design, research, writing becomes one tag per piece, and the text after the last comma stays in the field until Enter or another comma ends it. A piece that is already a tag is skipped, and once the list holds its max-tags, the rest of the pasted text stays in the field.

Pitfalls

  • Don't use tags-input for a closed taxonomy. If the user must pick from a fixed list, <x-wirekit::multi-select> is correct. Tags-input is for free-form entry (skills, keywords, emails).
  • Don't allow duplicates without thought. The component dedupes by string equality — case-sensitive. If you want case-insensitive dedup, normalize on the server before persisting.

Design Tokens

Token Purpose
--color-wk-bg-input Input area background
--color-wk-bg-muted Tag chip background
--color-wk-border-strong Default border color
--color-wk-border-error Border color on error
--color-wk-text Input and tag text color
--color-wk-text-muted Placeholder and remove icon color
--color-wk-ring Focus ring color
--radius-wk-md Input container border radius
--radius-wk-sm Tag chip border radius
--transition-wk-duration Add/remove chip transition
--opacity-wk-disabled Dimmed state when disabled

Customization

Override defaults in config/wirekit.php:

'components' => [
    'tags-input' => ['placeholder' => 'Type and press Enter...', 'max-tags' => 10],
],

Further Reading

Updated in WireKit v2.66.0 (2026-10-13)

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store