{
  "$schema": "https://mian.ro/schema/v2/registry.json",
  "schemaVersion": "2.0.0",
  "generatedAt": "2026-10-02T13:14:06.957Z",
  "id": "combobox",
  "name": "Combobox",
  "category": "Forms",
  "description": "Autocomplete: a text input that filters a listbox as you type. Keyboard navigation and aria-activedescendant included.",
  "tags": [
    "combobox",
    "autocomplete",
    "typeahead",
    "searchable select",
    "filter"
  ],
  "requiresJs": true,
  "license": "LicenseRef-MIAN-1.0",
  "composition": {
    "role": null,
    "intents": [
      "autocomplete",
      "searchable select",
      "typeahead",
      "filter as you type"
    ],
    "prominence": null,
    "density": null,
    "worksAfter": [],
    "worksBefore": [],
    "alternatives": [],
    "avoid": null
  },
  "url": "https://mian.ro/components/combobox",
  "markdownUrl": "https://mian.ro/ai/components/combobox.md",
  "classes": [
    {
      "name": "mn-combobox",
      "description": "Wrapper."
    },
    {
      "name": "mn-combobox-list",
      "description": "The listbox; children are role=\"option\"."
    },
    {
      "name": "mn-combobox-toggle",
      "description": "Optional chevron button."
    }
  ],
  "accessibility": [
    "Follows the WAI-ARIA combobox pattern: role=\"combobox\", aria-expanded, aria-controls, aria-activedescendant.",
    "ArrowDown opens and moves, Enter selects, Escape closes then clears. Focus never leaves the input.",
    "Emits mn:change with the selected value."
  ],
  "examples": [
    {
      "title": "Framework picker",
      "description": null,
      "html": "<div class=\"mn-field\" style=\"max-width:22rem\">\n  <label class=\"mn-label\" for=\"cb-1\">Framework</label>\n  <div class=\"mn-combobox\">\n    <input class=\"mn-input\" id=\"cb-1\" placeholder=\"Search frameworks…\">\n    <button class=\"mn-combobox-toggle\" type=\"button\" aria-label=\"Show frameworks\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"/></svg></button>\n    <ul class=\"mn-combobox-list\">\n      <li role=\"option\" data-value=\"astro\">Astro</li>\n      <li role=\"option\" data-value=\"laravel\">Laravel</li>\n      <li role=\"option\" data-value=\"next\">Next.js</li>\n      <li role=\"option\" data-value=\"nuxt\">Nuxt</li>\n      <li role=\"option\" data-value=\"rails\">Ruby on Rails</li>\n      <li role=\"option\" data-value=\"svelte\">SvelteKit</li>\n      <li role=\"option\" data-value=\"wordpress\">WordPress</li>\n    </ul>\n  </div>\n</div>"
    }
  ],
  "css": ".mn-combobox { position: relative; }\n.mn-combobox > .mn-input { padding-inline-end: 2.25rem; }\n.mn-combobox-toggle {\n  position: absolute; inset-block: 0; inset-inline-end: 0; width: 2.25rem;\n  display: grid; place-items: center; border: 0; background: none; color: var(--mn-muted); cursor: pointer;\n  border-radius: 0 var(--mn-radius-field) var(--mn-radius-field) 0;\n}\n.mn-combobox-toggle svg { width: 1rem; height: 1rem; transition: transform var(--mn-duration) var(--mn-ease); }\n.mn-combobox[data-open] .mn-combobox-toggle svg { transform: rotate(180deg); }\n.mn-combobox-list {\n  position: absolute; z-index: 50; inset-inline: 0; top: calc(100% + 4px);\n  max-height: 16rem; overflow: auto; margin: 0; padding: 4px; list-style: none;\n  border: var(--mn-border-w) solid var(--mn-border); border-radius: var(--mn-radius-box);\n  background: var(--mn-popover); box-shadow: var(--mn-shadow-lg);\n  font: 400 var(--mn-text-sm)/1.4 var(--mn-font);\n}\n.mn-combobox-list[hidden] { display: none; }\n.mn-combobox-list > [role=\"option\"] {\n  display: flex; align-items: center; gap: .5rem; padding: .45rem .6rem;\n  border-radius: calc(var(--mn-radius-box) - 4px); cursor: pointer;\n}\n.mn-combobox-list > [role=\"option\"][data-active] { background: var(--mn-hover); }\n.mn-combobox-list > [role=\"option\"][aria-selected=\"true\"] { font-weight: 600; }\n.mn-combobox-list > [role=\"option\"][aria-selected=\"true\"]::after { content: \"✓\"; margin-inline-start: auto; }\n.mn-combobox-list > [role=\"option\"][hidden] { display: none; }\n.mn-combobox-empty { padding: .6rem; color: var(--mn-muted); text-align: center; }",
  "js": "register('combobox', (root) => {\n  each(root, '.mn-combobox', 'Cbx', (w) => {\n    const input = w.querySelector('input');\n    const list = w.querySelector('.mn-combobox-list');\n    const toggle = w.querySelector('.mn-combobox-toggle');\n    if (!input || !list) return;\n    const opts = () => [...list.querySelectorAll('[role=\"option\"]')];\n    let empty = list.querySelector('.mn-combobox-empty');\n    if (!empty) { empty = document.createElement('li'); empty.className = 'mn-combobox-empty'; empty.textContent = w.dataset.mnEmpty || 'No results'; empty.hidden = true; empty.setAttribute('role', 'presentation'); list.appendChild(empty); }\n    input.setAttribute('role', 'combobox');\n    input.setAttribute('aria-autocomplete', 'list');\n    input.setAttribute('aria-controls', id(list, 'mn-lb'));\n    input.setAttribute('aria-expanded', 'false');\n    input.autocomplete = 'off';\n    list.setAttribute('role', 'listbox');\n    opts().forEach((o) => id(o, 'mn-opt'));\n    if (toggle) { toggle.tabIndex = -1; toggle.setAttribute('aria-label', toggle.getAttribute('aria-label') || 'Show options'); }\n\n    let active = -1;\n    const visible = () => opts().filter((o) => !o.hidden);\n    const setActive = (i) => {\n      const v = visible();\n      opts().forEach((o) => o.removeAttribute('data-active'));\n      active = v.length ? (i + v.length) % v.length : -1;\n      if (active >= 0) { v[active].setAttribute('data-active', ''); input.setAttribute('aria-activedescendant', v[active].id); v[active].scrollIntoView({ block: 'nearest' }); }\n      else input.removeAttribute('aria-activedescendant');\n    };\n    const open = () => { list.hidden = false; w.setAttribute('data-open', ''); input.setAttribute('aria-expanded', 'true'); };\n    const close = () => { list.hidden = true; w.removeAttribute('data-open'); input.setAttribute('aria-expanded', 'false'); setActive(-1); };\n    const filter = () => {\n      const q = input.value.trim().toLowerCase();\n      let n = 0;\n      opts().forEach((o) => { const hit = !q || o.textContent.toLowerCase().includes(q); o.hidden = !hit; if (hit) n++; });\n      empty.hidden = n > 0;\n    };\n    const choose = (o) => {\n      opts().forEach((x) => x.setAttribute('aria-selected', String(x === o)));\n      input.value = o.textContent.trim();\n      w.dataset.value = o.dataset.value ?? input.value;\n      close();\n      emit(w, 'change', { value: w.dataset.value, label: input.value });\n    };\n    list.hidden = true;\n    input.addEventListener('input', () => { filter(); open(); setActive(0); });\n    input.addEventListener('focus', () => { filter(); });\n    input.addEventListener('keydown', (e) => {\n      if (e.key === 'ArrowDown') { e.preventDefault(); if (list.hidden) { filter(); open(); } setActive(active + 1); }\n      else if (e.key === 'ArrowUp') { e.preventDefault(); if (list.hidden) { filter(); open(); } setActive(active - 1); }\n      else if (e.key === 'Enter' && !list.hidden && active >= 0) { e.preventDefault(); choose(visible()[active]); }\n      else if (e.key === 'Escape') { if (!list.hidden) { e.preventDefault(); close(); } else input.value = ''; }\n      else if (e.key === 'Tab') close();\n    });\n    list.addEventListener('pointerdown', (e) => e.preventDefault());   // keep focus in the input\n    list.addEventListener('click', (e) => { const o = e.target.closest('[role=\"option\"]'); if (o) choose(o); });\n    toggle?.addEventListener('click', () => { if (list.hidden) { input.value && filter(); open(); input.focus(); } else close(); });\n    input.addEventListener('blur', () => setTimeout(() => { if (!w.contains(document.activeElement)) close(); }, 0));\n  });\n});",
  "install": {
    "css": "https://mian.ro/dist/mian.css",
    "js": "https://mian.ro/dist/mian.js",
    "registryItem": "https://mian.ro/r/combobox.json"
  }
}
