Skip to content

Tags input

The base is a plain .re-input with data-re-tags-input. Without JavaScript it’s a normal text field that submits one comma-separated value. Opt into the editor:

import { enhanceTagsInput } from "@relements/core/behaviors/tags-input";
enhanceTagsInput(document);

Enhancing wraps the input in a role="group" shell: typing + Enter or comma commits a chip (reusing the tag visuals), Backspace on an empty field removes the last, and each tag is backed by a hidden input so the form submits an array (getAll(name)). The editor itself is unnamed, so it never contributes a stray value. Commits/removals fire a bubbling re-tags-change event with detail.values.

Options via data-*: data-re-tags-max, data-re-tags-name (the hidden-input name; defaults to the input’s), data-re-tags-allow-duplicates, data-re-tags-tone. Duplicates are rejected case-insensitively by default.

Press Enter or comma to add. Backspace removes the last.
Live example
<div class="re-field">
  <label class="re-field__label" for="tags-basic">Tags</label>
  <input
    class="re-input"
    type="text"
    id="tags-basic"
    name="tags"
    value="design, engineering"
    placeholder="Add tags, separated by commas"
    autocomplete="off"
    data-re-tags-input
  />
  <span class="re-field__hint">Press Enter or comma to add. Backspace removes the last.</span>
</div>

At the limit, further entries are rejected and the field is marked invalid.

Live example
<div class="re-field">
  <label class="re-field__label" for="tags-max">Skills</label>
  <input
    class="re-input"
    type="text"
    id="tags-max"
    name="skills"
    value="html, css"
    placeholder="Up to 3"
    autocomplete="off"
    data-re-tags-input
    data-re-tags-max="3"
  />
</div>

Submitting sends one name entry per tag.

Live example
<form>
  <div class="re-field">
    <label class="re-field__label" for="tags-form">Labels</label>
    <input
      class="re-input"
      type="text"
      id="tags-form"
      name="labels"
      value="bug, docs"
      autocomplete="off"
      data-re-tags-input
    />
  </div>
  <button class="re-button" type="submit" style="margin-block-start: var(--re-space-3)">
    Submit
  </button>
</form>
  • Keyboard — the editor is a native text field, so it focuses with Tab and types normally. Enter or a typed/pasted comma commits the current text as a chip; Backspace on an empty field removes the last chip. Each chip’s × is a real <button> (Tab to reach it, Enter/Space to activate); removing a chip by its button moves focus to the previous chip’s remove button, or back to the editor when none remain.
  • Focus — the whole group shows the visible focus ring on :focus-within (the editor sheds its own ring so the shell owns it); chip remove buttons get the same ring on :focus-visible. At the data-re-tags-max limit the group is marked [data-invalid] and the ring turns danger-colored.
  • Semantics — enhancing wraps the input in a role="group" labelled from the field’s <label for> via aria-labelledby, so AT announces the group by its label. Each × button carries an aria-label="Remove <tag>", and a visually hidden (.re-sr-only) aria-live="polite" region announces “Added …”, “Removed …”, and “Maximum N tags reached” as you edit. Committed tags are backed by hidden inputs that carry the submitted array values.
  • Notes — pre-seeded tags are not announced on enhance, only edits you make. Without JavaScript the base degrades to a plain labelled .re-input. See the accessibility guide.