Exclusifbêta
Tag Input <mv-tag-input>
Champ à puces pour les étiquettes, les destinataires, les compétences et les mots-clés : Entrée, virgule ou Tab ajoute, coller « a, b; c » (ou une colonne copiée depuis un tableur) ajoute chaque élément, Retour arrière sélectionne puis supprime la dernière puce, les flèches parcourent les puces, avec en option un motif, une limite et un dédoublonnage insensible à la casse et aux accents qui fait clignoter la puce existante, et des suggestions natives <datalist>. Ce que d’autres oublient : c’est un vrai champ de formulaire (ElementInternals : entrées name répétées ou une seule valeur jointe par un séparateur, reset, restauration avant/arrière, validité required et pattern), le texte saisi n’est jamais perdu (à la perte du focus ou à l’envoi, il devient une étiquette, ou reste ; le texte en trop ou refusé retourne dans le champ), les séparateurs sont interceptés depuis les claviers mobiles et IME et dans la ponctuation arabe et CJK, et les annonces restent courtes (« research supprimé, 2 étiquettes »).
| Catégorie | Formulaires |
|---|---|
| Type | Web Component (<mv-tag-input>) |
| Statut | bêta |
| Keywords | exclusive, light, tags, chips, tokens, multi-value, recipients, keywords, form-associated, paste |
When to use
- A form collects several free-form values such as tags, keywords, skills or email recipients and must submit them
- Users paste lists from a spreadsheet, an email client or a document and expect each entry to become a tag
- Entries must match a format such as email addresses, with invalid ones kept and flagged rather than dropped
- A filter bar or settings page needs removable keyword chips that work fully from the keyboard
Avoid when
- Values must be picked from a long or remote list with filtering, groups and option rendering → use Combobox instead
- The field is a storefront search with recent searches and product results → use Search Autocomplete instead
- Only a fixed handful of options can be chosen, with nothing typed → use Checkbox instead
Installation
node scripts/add.mjs tag-input --out ./src/marvelousAgent IA avec le serveur MCP de Marvelous UI : install_components({ slugs: ["tag-input"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Fichiers copiés (dépendances comprises) : tokens/tokens.css, core/base.css, core/dom.js, core/element.js, components/tag-input/tag-input.js, components/tag-input/tag-input.css.
Utilisation
Démarrage rapide, le balisage minimal qui fonctionne :
<mv-tag-input name="tags" value="design, research"></mv-tag-input>Balisage de référence : partez de celui-ci et personnalisez-le avec les attributs, data-* et les variables CSS :
<form id="mv-tag-input-demo" style="display:grid;gap:1.1rem;width:min(100%,34rem);padding:1.25rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-xl);background:var(--mv-surface);box-shadow:var(--mv-shadow-sm)">
<div style="display:grid;gap:.4rem">
<label for="mv-tag-demo-topics" style="font-size:.875rem;font-weight:600">Topics</label>
<mv-tag-input id="mv-tag-demo-topics" name="topics" value="design, research" unique max="6" required placeholder="Add a topic…">
<datalist>
<option value="Accessibility"></option>
<option value="Branding"></option>
<option value="Café culture"></option>
<option value="Localization"></option>
<option value="Pricing"></option>
<option value="Typography"></option>
</datalist>
</mv-tag-input>
<small style="color:var(--mv-fg-muted);font-size:.8125rem">Up to 6. Paste “ux, pricing; Café culture” to add several; typing “Design” flashes the existing tag.</small>
</div>
<div style="display:grid;gap:.4rem">
<label for="mv-tag-demo-invite" style="font-size:.875rem;font-weight:600">Invite reviewers</label>
<mv-tag-input id="mv-tag-demo-invite" name="reviewers" separator=";" unique pattern="[^\s@]+@[^\s@]+\.[^\s@]{2,}" placeholder="[email protected]"
value="[email protected]; [email protected]; lucia.fernandez@lumen"></mv-tag-input>
<small style="color:var(--mv-fg-muted);font-size:.8125rem">Emails only, sent as one “;”-separated value. Invalid addresses stay, flagged, so nothing is lost.</small>
</div>
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:.5rem">
<button class="mv-button" type="submit">Save project</button>
<button class="mv-button" data-variant="ghost" type="reset">Reset</button>
<output id="mv-tag-demo-out" style="flex-basis:100%;min-height:1.2em;font:.75rem/1.5 var(--mv-font-mono);color:var(--mv-fg-muted);overflow-wrap:anywhere"></output>
</div>
</form>
<script type="module">
const form = document.getElementById("mv-tag-input-demo");
const out = document.getElementById("mv-tag-demo-out");
form.addEventListener("submit", (e) => {
e.preventDefault();
const fd = new FormData(form);
out.textContent = [...fd].map(([k, v]) => `${k}=${v}`).join(" · ");
});
form.addEventListener("invalid", () => { out.textContent = ""; }, true);
</script>API
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
name | string | Form field name. Without separator, each tag is submitted as its own name entry (like a multiple select). | |
value | string | Initial tags, split on commas, semicolons, line breaks, tabs (and separator); also the value restored by form reset. | |
separator | string | Submit one value with the tags joined by this string (e.g. “,” or “;”) instead of repeated entries; it also splits typed and pasted text. | |
max | number | Maximum number of tags; extra text stays in the field and “Limit reached” is announced. | |
pattern | regex | Each tag must fully match (like input pattern). Mismatches are still added, flagged visually and as “(invalid)”, and make the field invalid (patternMismatch). | |
unique | boolean | No duplicates, compared without case or accents (“cafe” = “Café”); the existing chip flashes instead. | |
pending | commit | keep | commit | What happens to typed text when focus leaves or the form submits: it becomes a tag (commit) or stays in the field (keep). |
placeholder | string | Add a tag… | Shown while there are no tags (defaults to strings.placeholder). |
required / disabled | boolean | Standard form behavior: valueMissing with no tags; disabled (also from a disabled fieldset) makes the field and chips inert. | |
data-count / data-full / data-invalid | set by the component | Tag count, limit reached, at least one invalid tag (for styling). |
Properties
| Name | Type | Description |
|---|---|---|
tags | string[] | Current tags (a copy). Setting it replaces them silently, like a native field value. Can be set before the element is defined. |
value | string | Tags joined with separator (default “, ”); setting accepts a string or an array. |
suggestions | string[] | Suggestions shown in the native datalist popup (already added ones are hidden); picking one adds it. A <datalist> child works too. |
strings | object | Overrides for every visible and announced text: placeholder, hint, remove (“Remove {tag}”), invalidTag, added, addedInvalid, addedMany, removed, duplicate, full, required, mismatch, overflow. Plural entries are objects keyed by Intl.PluralRules category ({ zero?, one, other… }); numbers are formatted with Intl in the nearest lang. |
form / labels / validity / validationMessage / willValidate | read-only | Standard form-control properties. |
Methods
| Name | Description |
|---|---|
add(text | string[]) | Adds tags as the user would (split, dedupe, limit, mv-add veto, announcement); returns the tags added. |
remove(tag | index) | Removes a tag by value (case- and accent-insensitive) or index. |
clear() | Removes every tag (silent). |
checkValidity() / reportValidity() | Standard; the browser bubble points at the text field. |
focus() | Focuses the text field. |
Events
| Name | Description |
|---|---|
mv-add | Cancelable, before a user-added tag is created; detail = { value }. Cancel to veto (the text returns to the field). |
mv-remove | Cancelable, before a tag is removed; detail = { value, index }. |
mv-change | After tags were added or removed by the user; detail = { tags, value }. A native change event fires on the element too. |
Content structure
| Name | Description |
|---|---|
datalist | Optional <datalist> child with <option value> suggestions. |
CSS classes
| Name | Description |
|---|---|
mv-tag-input-list | The <ul role=list> of chips (display: contents, so chips and field share one wrapping row). |
mv-tag-input-chip / -text / -remove | A chip (li; data-invalid, data-flash), its label and its remove button. |
mv-tag-input-field | The text input. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-tag-input-chip-bg | var(--mv-bg-muted) | Chip background. |
--mv-tag-input-chip-fg | var(--mv-fg) | Chip text. |
--mv-tag-input-flash | var(--mv-accent) | Highlight of the existing chip when a duplicate is typed. |
--mv-tag-input-invalid | var(--mv-danger) | Invalid chip and field color. |
--mv-input-radius / --mv-input-bg | var(--mv-radius-md) / field background | Shared with mv-input, so both fields match. |
Accessibility
The host is a form-associated custom element: a <label for> names it (the inner text field is aria-labelledby the label, or takes the host’s aria-label), clicking the label or the empty part of the box focuses the field, and validity messages anchor to the field. Chips are a real list (ul role=list), so screen readers hear “list, 2 items”; each chip has a real button named “Remove design” (the × icon is aria-hidden). Only the text field is in the Tab order: Left arrow at the start of the field, or Backspace in an empty field, moves focus to the last chip’s remove button; Left/Right (mirrored in RTL), Home and End walk the chips, Right past the last one returns to the field; Backspace removes and moves left, Delete and Enter remove and stay in place, Escape or typing a character goes back to the field. A short screen-reader hint on the field says Enter or comma adds and Left arrow reaches the tags. One polite status region announces only results, briefly: “design added”, “3 tags added”, “research removed, 2 tags”, “Design is already added”, “Limit reached: 6 tags”. Invalid chips are flagged by a “!” mark, a wavy underline and the text “(invalid)”, not by color alone, and the field gets aria-invalid. Focus is always visible (the whole chip is outlined when its button has focus); forced colors use system outlines, a dashed border for invalid chips and a CanvasText mark. Chip entry and the duplicate flash scale only with --mv-motion and stop under prefers-reduced-motion or [data-motion=reduce] (the highlight still shows, statically). Enter never submits while text is pending; with an empty field it submits the form as usual. IME composition is respected.