{
  "$schema": "https://mian.ro/schema/v2/registry.json",
  "schemaVersion": "2.0.0",
  "generatedAt": "2026-10-02T13:14:06.957Z",
  "id": "tag-input",
  "name": "Tag input",
  "category": "Forms",
  "description": "Type to add tags as removable chips. Enter or comma adds, Backspace removes, pasted lists are split.",
  "tags": [
    "tag input",
    "tags",
    "chips input",
    "token field",
    "multi value",
    "labels",
    "keywords"
  ],
  "requiresJs": true,
  "license": "LicenseRef-MIAN-1.0",
  "composition": {
    "role": null,
    "intents": [
      "tags input",
      "add keywords",
      "multi value input",
      "email recipients"
    ],
    "prominence": null,
    "density": null,
    "worksAfter": [],
    "worksBefore": [],
    "alternatives": [],
    "avoid": null
  },
  "url": "https://mian.ro/components/tag-input",
  "markdownUrl": "https://mian.ro/ai/components/tag-input.md",
  "classes": [
    {
      "name": "mn-tag-input",
      "description": "Wrapper around chips and one input. Add mn-input to style it as a field."
    },
    {
      "name": "data-name=\"tags\"",
      "description": "Adds hidden inputs named tags[] for form submission."
    },
    {
      "name": "data-max=\"5\"",
      "description": "Maximum number of tags."
    }
  ],
  "accessibility": [
    "Give the input a label (visible, or aria-label). Each chip’s remove button is labelled “Remove …”.",
    "Additions and removals are announced in a polite live region.",
    "Emits mn:change with { values }."
  ],
  "examples": [
    {
      "title": "Tags",
      "description": null,
      "html": "<div class=\"mn-field\" style=\"max-width:26rem\">\n  <label class=\"mn-label\" for=\"tags-1\">Topics</label>\n  <div class=\"mn-input mn-tag-input\" data-name=\"topics\">\n    <span class=\"mn-chip\" data-value=\"Design\">Design<button class=\"mn-chip-remove\" type=\"button\" aria-label=\"Remove Design\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg></button></span>\n    <span class=\"mn-chip\" data-value=\"Accessibility\">Accessibility<button class=\"mn-chip-remove\" type=\"button\" aria-label=\"Remove Accessibility\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg></button></span>\n    <input id=\"tags-1\" placeholder=\"Add a topic…\">\n  </div>\n  <p class=\"mn-hint\">Press Enter or comma to add.</p>\n</div>"
    }
  ],
  "css": ".mn-tag-input {\n  display: flex; flex-wrap: wrap; align-items: center; gap: .35rem;\n  min-height: var(--mn-size-md); padding: .3rem .4rem; cursor: text;\n}\n.mn-tag-input.mn-input { height: auto; }\n.mn-tag-input > input { flex: 1 1 6rem; min-width: 6rem; height: calc(var(--mn-size-md) - .8rem); padding: 0 .25rem; border: 0; outline: 0; background: none; color: inherit; font: inherit; }\n.mn-tag-input:focus-within { border-color: var(--mn-ring); box-shadow: 0 0 0 3px color-mix(in oklab, var(--mn-ring) 25%, transparent); }\n.mn-tag-input .mn-chip { height: calc(var(--mn-size-md) - .9rem); }",
  "js": "/* <div class=\"mn-input mn-tag-input\" data-name=\"tags\"><input aria-label=\"Add a tag\"></div>\n   Enter or comma adds a tag, Backspace in an empty field removes the last\n   one, pasted comma lists are split. With data-name, hidden inputs named\n   tags[] are kept in sync for forms. Emits mn:change { values }. */\nregister('tag-input', (root) => {\n  each(root, '.mn-tag-input', 'Tag', (w) => {\n    const input = w.querySelector('input:not([type=\"hidden\"])');\n    if (!input) return;\n    const name = w.dataset.name;\n    const max = Number(w.dataset.max || 0);\n    const chips = () => [...w.querySelectorAll('.mn-chip')];\n    const values = () => chips().map((c) => c.dataset.value ?? c.firstChild?.textContent.trim() ?? '');\n    const sync = (quiet) => {\n      w.querySelectorAll('input[type=\"hidden\"]').forEach((h) => h.remove());\n      if (name) for (const v of values()) { const h = document.createElement('input'); h.type = 'hidden'; h.name = `${name}[]`; h.value = v; w.appendChild(h); }\n      if (!quiet) emit(w, 'change', { values: values() });\n    };\n    const add = (text) => {\n      const v = text.trim();\n      if (!v || values().includes(v) || (max && chips().length >= max)) return false;\n      const chip = document.createElement('span');\n      chip.className = 'mn-chip'; chip.dataset.value = v; chip.append(v);\n      const x = document.createElement('button'); x.type = 'button'; x.className = 'mn-chip-remove'; x.setAttribute('aria-label', `Remove ${v}`); x.innerHTML = ICON_X;\n      chip.appendChild(x);\n      input.before(chip);\n      announce(`Added ${v}`);\n      return true;\n    };\n    const commit = () => { let changed = false; for (const part of input.value.split(',')) changed = add(part) || changed; input.value = ''; if (changed) sync(); };\n    input.addEventListener('keydown', (e) => {\n      if ((e.key === 'Enter' || e.key === ',') && input.value.trim()) { e.preventDefault(); commit(); }\n      else if (e.key === 'Backspace' && !input.value) { const last = chips().pop(); if (last) { const v = last.dataset.value; last.remove(); announce(`Removed ${v}`); sync(); } }\n    });\n    input.addEventListener('paste', () => setTimeout(() => { if (input.value.includes(',')) commit(); }));\n    input.addEventListener('blur', () => { if (input.value.trim()) commit(); });\n    w.addEventListener('mn:remove', () => setTimeout(() => { sync(); if (!chips().length) input.focus(); }));\n    w.addEventListener('click', (e) => { if (e.target === w) input.focus(); });\n    sync(true);\n  });\n});",
  "install": {
    "css": "https://mian.ro/dist/mian.css",
    "js": "https://mian.ro/dist/mian.js",
    "registryItem": "https://mian.ro/r/tag-input.json"
  }
}
