Exclusivobeta
Tag Input <mv-tag-input>
Campo de chips para etiquetas, destinatarios, habilidades y palabras clave: Enter, coma o Tab añaden, al pegar “a, b; c” (o una columna copiada de una hoja de cálculo) se añade cada una, Backspace selecciona y luego elimina el último chip, las flechas recorren los chips, con patrón y límite opcionales, deduplicación que ignora mayúsculas y acentos y hace destellar el chip existente, y sugerencias nativas de <datalist>. Lo que a otros se les escapa: es un campo de formulario real (ElementInternals: entradas name repetidas o un solo valor unido por un separador, reset, restauración al ir atrás o adelante, validez required y pattern), el texto escrito nunca se pierde (al perder el foco o al enviar se convierte en etiqueta, o se queda; el texto sobrante y el vetado vuelven al campo), los separadores se detectan desde teclados móviles e IME y en la puntuación árabe y CJK, y los anuncios son breves (“research eliminada, 2 etiquetas”).
| Categoría | Formularios |
|---|---|
| Tipo | Web Component (<mv-tag-input>) |
| Estado | beta |
| 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
Instalación
node scripts/add.mjs tag-input --out ./src/marvelousAgente de IA con el servidor MCP de Marvelous UI: install_components({ slugs: ["tag-input"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Archivos copiados (dependencias incluidas): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, components/tag-input/tag-input.js, components/tag-input/tag-input.css.
Uso
Inicio rápido, el marcado mínimo que funciona:
<mv-tag-input name="tags" value="design, research"></mv-tag-input>Marcado de referencia: parte de él y personalízalo con atributos, data-* y 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 | Tipo | 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 | Tipo | 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.