Open demo

CSS code

HTML
<div><span class="lbl">Counts, badge and a clear button</span><div class="card">
<div class="ms" data-select data-names="9">
  <div class="ms-head"><label class="ms-label" id="ms-am-label" for="ms-am-native">Amenities</label><button type="button" class="ms-clear" data-clear aria-label="Clear amenities" hidden>Clear</button></div>
  <select class="ms-native" id="ms-am-native" name="amenities" multiple size="8">
    <option value="wifi" selected>Wi-Fi</option><option value="parking">Free parking</option><option value="kitchen" selected>Kitchen</option><option value="washer">Washing machine</option>
    <option value="ac" selected>Air conditioning</option><option value="pool">Pool</option><option value="ev">EV charger</option><option value="pets">Pets allowed</option>
  </select>
  <div class="ms-ui" data-ui hidden>
    <div class="ms-combo" id="ms-am-combo" role="combobox" tabindex="0" aria-haspopup="listbox" aria-expanded="false" aria-controls="ms-am-list" aria-labelledby="ms-am-label">
      <svg class="ms-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 10.5 12 3l9 7.5"/><path d="M5 9v11h14V9"/><path d="M10 20v-6h4v6"/></svg>
      <span class="ms-shown" data-shown data-placeholder="Any amenities">Any amenities</span>
      <span class="ms-count" data-count aria-hidden="true" hidden>0</span>
      <svg class="ms-chev" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m4 6 4 4 4-4"/></svg>
    </div>
    <div class="ms-pop" data-popup hidden>
      <div class="ms-list" role="listbox" id="ms-am-list" aria-labelledby="ms-am-label" aria-multiselectable="true" tabindex="-1">
        <div class="ms-opt" role="option" id="ms-am-1" data-value="wifi" data-label="Wi-Fi" aria-selected="false"><span class="ms-box" aria-hidden="true"></span>Wi-Fi <span class="ms-num">128 stays</span></div>
        <div class="ms-opt" role="option" id="ms-am-2" data-value="parking" data-label="Free parking" aria-selected="false"><span class="ms-box" aria-hidden="true"></span>Free parking <span class="ms-num">64 stays</span></div>
        <div class="ms-opt" role="option" id="ms-am-3" data-value="kitchen" data-label="Kitchen" aria-selected="false"><span class="ms-box" aria-hidden="true"></span>Kitchen <span class="ms-num">91 stays</span></div>
        <div class="ms-opt" role="option" id="ms-am-4" data-value="washer" data-label="Washing machine" aria-selected="false"><span class="ms-box" aria-hidden="true"></span>Washing machine <span class="ms-num">47 stays</span></div>
        <div class="ms-opt" role="option" id="ms-am-5" data-value="ac" data-label="Air conditioning" aria-selected="false"><span class="ms-box" aria-hidden="true"></span>Air conditioning <span class="ms-num">73 stays</span></div>
        <div class="ms-opt" role="option" id="ms-am-6" data-value="pool" data-label="Pool" aria-selected="false"><span class="ms-box" aria-hidden="true"></span>Pool <span class="ms-num">12 stays</span></div>
        <div class="ms-opt" role="option" id="ms-am-7" data-value="ev" data-label="EV charger" aria-selected="false"><span class="ms-box" aria-hidden="true"></span>EV charger <span class="ms-num">9 stays</span></div>
        <div class="ms-opt" role="option" id="ms-am-8" data-value="pets" data-label="Pets allowed" aria-selected="false"><span class="ms-box" aria-hidden="true"></span>Pets allowed <span class="ms-num">38 stays</span></div>
      </div>
    </div>
  </div>
</div>
</div></div>

<div><span class="lbl">Checkbox dropdown with select all</span><div class="card">
<div class="ms" data-select>
  <label class="ms-label" id="ms-status-label" for="ms-status-native">Status</label>
  <select class="ms-native" id="ms-status-native" name="status" multiple size="6">
    <option value="backlog">Backlog</option><option value="todo">To do</option><option value="progress" selected>In progress</option>
    <option value="review" selected>In review</option><option value="done">Done</option><option value="cancelled">Cancelled</option>
  </select>
  <div class="ms-ui" data-ui hidden>
    <div class="ms-combo" id="ms-status-combo" role="combobox" tabindex="0" aria-haspopup="listbox" aria-expanded="false" aria-controls="ms-status-list" aria-labelledby="ms-status-label">
      <span class="ms-shown" data-shown data-placeholder="Any status" data-all-text="All statuses">Any status</span>
      <svg class="ms-chev" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m4 6 4 4 4-4"/></svg>
    </div>
    <div class="ms-pop" data-popup hidden>
      <div class="ms-list" role="listbox" id="ms-status-list" aria-labelledby="ms-status-label" aria-multiselectable="true" tabindex="-1">
        <div class="ms-opt ms-all" role="option" id="ms-status-all" data-all aria-selected="false"><span class="ms-box" aria-hidden="true"></span>Select all</div>
        <div class="ms-opt" role="option" id="ms-status-1" data-value="backlog" aria-selected="false"><span class="ms-box" aria-hidden="true"></span><span class="ms-dot" style="--dot:#64748b" aria-hidden="true"></span>Backlog</div>
        <div class="ms-opt" role="option" id="ms-status-2" data-value="todo" aria-selected="false"><span class="ms-box" aria-hidden="true"></span><span class="ms-dot" style="--dot:#2563eb" aria-hidden="true"></span>To do</div>
        <div class="ms-opt" role="option" id="ms-status-3" data-value="progress" aria-selected="false"><span class="ms-box" aria-hidden="true"></span><span class="ms-dot" style="--dot:#d97706" aria-hidden="true"></span>In progress</div>
        <div class="ms-opt" role="option" id="ms-status-4" data-value="review" aria-selected="false"><span class="ms-box" aria-hidden="true"></span><span class="ms-dot" style="--dot:#7c3aed" aria-hidden="true"></span>In review</div>
        <div class="ms-opt" role="option" id="ms-status-5" data-value="done" aria-selected="false"><span class="ms-box" aria-hidden="true"></span><span class="ms-dot" style="--dot:#16a34a" aria-hidden="true"></span>Done</div>
        <div class="ms-opt" role="option" id="ms-status-6" data-value="cancelled" aria-selected="false"><span class="ms-box" aria-hidden="true"></span><span class="ms-dot" style="--dot:#dc2626" aria-hidden="true"></span>Cancelled</div>
      </div>
      <p class="ms-foot" aria-hidden="true"><span><kbd>Space</kbd>tick</span><span><kbd>Ctrl A</kbd>all</span><span><kbd>Esc</kbd>close</span></p>
    </div>
  </div>
</div>
</div></div>
CSS
/* Multiselect dropdown with checkboxes: a select-only combobox (WAI-ARIA APG) that opens a multi-select listbox.
   The ticks, the highlighted row and the summary are drawn from aria-expanded, data-active and data-chosen,
   which the script sets. A hidden <select multiple> keeps the real form value. */
.ms{--ms-accent:#0f766e;--ms-tint:#e2f1ee;--ms-ring:rgba(15,118,110,.28);--ms-line:#859190;--ms-ink:#15202b;--ms-muted:#56615f;
  --ms-tick:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m3.5 8.5 3 3 6-7'/%3E%3C/svg%3E");
  --ms-dash:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-width='2.4' stroke-linecap='round'%3E%3Cpath d='M4 8h8'/%3E%3C/svg%3E");
  position:relative;display:grid;grid-template-columns:minmax(0,1fr);gap:7px;color:var(--ms-ink)}
.ms [hidden]{display:none!important}
.ms-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
.ms-label{font-size:.9rem;font-weight:600}
.ms-clear{padding:2px 4px;margin:-2px -4px;font:inherit;font-size:.82rem;font-weight:600;color:var(--ms-accent);background:none;border:0;border-radius:6px;cursor:pointer}
.ms-clear:hover{text-decoration:underline}
.ms-clear:focus-visible{outline:2px solid var(--ms-accent);outline-offset:1px}
/* without JavaScript the native list stays visible and does the job */
.ms-native{width:100%;padding:6px;font:inherit;color:inherit;background:#fff;border:1px solid var(--ms-line);border-radius:10px}
.ms-ui{position:relative}
.ms-combo{display:flex;align-items:center;gap:10px;height:44px;padding:0 14px;font-size:.95rem;background:#fff;border:1px solid var(--ms-line);
  border-radius:10px;box-shadow:0 1px 2px rgba(15,30,40,.06);cursor:pointer;user-select:none;transition:border-color .15s,box-shadow .15s}
.ms-combo:hover{border-color:#5b6766}
.ms-combo:focus-visible,.ms-combo[aria-expanded="true"]{outline:none;border-color:var(--ms-accent);box-shadow:0 0 0 4px var(--ms-ring)}
.ms-ico{flex:none;width:18px;height:18px;color:var(--ms-accent)}
.ms-shown{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ms-combo[data-empty] .ms-shown{color:var(--ms-muted)}
.ms-count{flex:none;display:grid;place-items:center;min-width:22px;height:22px;padding:0 7px;border-radius:999px;background:var(--ms-accent);color:#fff;font-size:.75rem;font-weight:700}
.ms-chev{flex:none;width:16px;height:16px;color:var(--ms-muted);transition:transform .2s}
.ms-combo[aria-expanded="true"] .ms-chev{transform:rotate(180deg)}
/* the popup and its options */
.ms-pop{position:absolute;z-index:30;left:0;right:0;top:calc(100% + 6px);padding:6px;background:#fff;border:1px solid #d3dbd9;border-radius:12px;
  box-shadow:0 18px 40px -12px rgba(15,30,40,.3)}
.ms-list{max-height:264px;overflow-y:auto;overscroll-behavior:contain;outline:none}
.ms-opt{display:flex;align-items:center;gap:11px;padding:8px 10px;border-radius:8px;font-size:.93rem;cursor:pointer}
.ms-opt[data-active]{background:var(--ms-tint)}
.ms-box{flex:none;width:18px;height:18px;border:1.5px solid #77817f;border-radius:5px;background:#fff center/13px no-repeat}
.ms-opt[data-chosen] .ms-box{background-color:var(--ms-accent);background-image:var(--ms-tick);border-color:var(--ms-accent)}
.ms-opt[data-mixed] .ms-box{background-color:var(--ms-accent);background-image:var(--ms-dash);border-color:var(--ms-accent)}
.ms-all{position:relative;margin-bottom:7px;font-weight:600}
.ms-all::after{content:"";position:absolute;left:10px;right:10px;bottom:-4px;height:1px;background:#e0e6e5}
.ms-dot{flex:none;width:9px;height:9px;margin-left:-3px;border-radius:50%;background:var(--dot)}
.ms-num{margin-left:auto;font-size:.8rem;color:var(--ms-muted);font-variant-numeric:tabular-nums}
.ms-foot{display:flex;gap:16px;padding:9px 10px 3px;margin-top:6px;border-top:1px solid #e0e6e5;font-size:.76rem;color:var(--ms-muted)}
.ms-foot kbd{padding:1px 6px;margin-right:4px;font:inherit;font-weight:600;color:var(--ms-ink);background:#f1f5f4;border:1px solid #ccd5d3;border-radius:5px}
@media (prefers-reduced-motion:reduce){.ms-combo,.ms-chev{transition:none}}
JavaScript
// Select-only combobox with a listbox popup (WAI-ARIA APG), single or multi-select.
// Focus stays on the combobox; aria-activedescendant points at the highlighted option.
// Closed: Down, Up, Enter or Space open it, Home / End open at the first / last option, typing opens and jumps.
// Open: Down / Up / Home / End / Page Up / Page Down move; Enter or Space picks (ticks in a multiselect);
// Ctrl+A ticks all; typing jumps to the next match; Escape closes; Tab closes and moves on.
// The <select> inside each [data-select] holds the real form value and the starting selection. It is hidden
// once this script runs, so without JavaScript the native control is still there and still works.
(() => {
  document.querySelectorAll('[data-select]').forEach(root => {
    const native = root.querySelector('select');
    const label = root.querySelector('label');
    const combo = root.querySelector('[role="combobox"]');
    const popup = root.querySelector('[data-popup]');
    const list = root.querySelector('[role="listbox"]');
    const shown = root.querySelector('[data-shown]');
    const count = root.querySelector('[data-count]');
    const clear = root.querySelector('[data-clear]');
    const multi = native.multiple;
    const opts = [...list.querySelectorAll('[role="option"]')];
    const all = list.querySelector('[data-all]');
    const items = opts.filter(o => o !== all);
    const source = o => [...native.options].find(n => n.value === o.dataset.value);
    const text = o => o.dataset.label || o.textContent.trim();
    const isOpen = () => combo.getAttribute('aria-expanded') === 'true';
    const changed = () => native.dispatchEvent(new Event('change', { bubbles: true }));
    let active = -1, typed = '', typedAt = 0;

    native.hidden = true;
    root.querySelector('[data-ui]').hidden = false;

    function render() {
      const on = items.filter(o => source(o).selected);
      items.forEach(o => {
        o.toggleAttribute('data-chosen', on.includes(o));
        if (multi || !isOpen()) o.setAttribute('aria-selected', on.includes(o));
      });
      if (all) {
        all.setAttribute('aria-selected', on.length === items.length);
        all.toggleAttribute('data-chosen', on.length === items.length);
        all.toggleAttribute('data-mixed', on.length > 0 && on.length < items.length);
      }
      if (!on.length) shown.textContent = shown.dataset.placeholder;
      else if (!multi) {
        const pic = on[0].querySelector('[data-pic]');
        shown.replaceChildren(...(pic ? [pic.cloneNode(true)] : []), text(on[0]));
      } else if (on.length === items.length && shown.dataset.allText) shown.textContent = shown.dataset.allText;
      else shown.textContent = on.length <= (+root.dataset.names || 2) ? on.map(text).join(', ') : on.length + ' selected';
      combo.toggleAttribute('data-empty', !on.length);
      if (count) { count.textContent = on.length; count.hidden = !on.length; }
      if (clear) clear.hidden = !on.length;
    }
    function setActive(i) {
      active = i;
      opts.forEach((o, n) => {
        o.toggleAttribute('data-active', n === i);
        if (!multi && i > -1) o.setAttribute('aria-selected', n === i); // single select: selection follows the highlight
      });
      if (i < 0) return combo.removeAttribute('aria-activedescendant');
      combo.setAttribute('aria-activedescendant', opts[i].id);
      const o = opts[i].getBoundingClientRect(), b = list.getBoundingClientRect(); // keep it in view without scrolling the page
      if (i === 0) list.scrollTop = 0;
      else if (o.top < b.top) list.scrollTop -= b.top - o.top;
      else if (o.bottom > b.bottom) list.scrollTop += o.bottom - b.bottom;
    }
    function open(i) {
      if (!isOpen()) { popup.hidden = false; combo.setAttribute('aria-expanded', 'true'); }
      if (i !== undefined) setActive(Math.max(0, Math.min(opts.length - 1, i)));
    }
    function close() {
      popup.hidden = true;
      combo.setAttribute('aria-expanded', 'false');
      setActive(-1);
      render();
    }
    const current = () => Math.max(0, opts.findIndex(o => o.hasAttribute('data-chosen')));
    function setAll() {
      const every = items.every(o => source(o).selected);
      items.forEach(o => { source(o).selected = !every; });
    }
    function pick(o) {
      if (!multi) native.value = o.dataset.value;
      else if (o === all) setAll();
      else source(o).selected = !source(o).selected;
      render();
      changed();
    }
    function typeahead(ch) {
      const now = Date.now();
      typed = (now - typedAt > 600 ? '' : typed) + ch.toLowerCase();
      typedAt = now;
      const same = [...typed].every(c => c === typed[0]); // pressing one letter again cycles through its matches
      const q = same ? typed[0] : typed;
      const from = active < 0 ? 0 : same ? active + 1 : active;
      for (let k = 0; k < opts.length; k++) {
        const n = (from + k) % opts.length;
        if (text(opts[n]).toLowerCase().startsWith(q)) return open(n);
      }
    }
    const typing = () => typed && Date.now() - typedAt < 600 && opts.some(o => text(o).toLowerCase().startsWith(typed + ' '));

    combo.addEventListener('keydown', e => {
      const k = e.key, last = opts.length - 1;
      const printable = k.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey;
      if (k.length > 1) typed = ''; // arrows, Home, End… end a type-ahead run
      if (!isOpen()) {
        if (k === 'ArrowDown' || k === 'ArrowUp' || k === 'Enter' || k === ' ') { e.preventDefault(); open(e.altKey ? undefined : current()); }
        else if (k === 'Home') { e.preventDefault(); open(0); }
        else if (k === 'End') { e.preventDefault(); open(last); }
        else if (printable) { e.preventDefault(); open(); typeahead(k); }
        return;
      }
      if (k === ' ' && typing()) { e.preventDefault(); return typeahead(k); } // a space inside a name, as in "In review"
      switch (k) {
        case 'ArrowDown': e.preventDefault(); setActive(Math.min(active + 1, last)); break;
        case 'ArrowUp':
          e.preventDefault();
          if (e.altKey) { if (!multi && active > -1) pick(opts[active]); close(); }
          else setActive(Math.max(active - 1, 0));
          break;
        case 'Home': e.preventDefault(); setActive(0); break;
        case 'End': e.preventDefault(); setActive(last); break;
        case 'PageDown': e.preventDefault(); setActive(Math.min(active + 10, last)); break;
        case 'PageUp': e.preventDefault(); setActive(Math.max(active - 10, 0)); break;
        case 'Enter': case ' ':
          e.preventDefault();
          typed = '';
          if (active > -1) pick(opts[active]);
          if (!multi) close();
          break;
        case 'Escape': e.preventDefault(); close(); break;
        case 'Tab': if (!multi && active > -1) pick(opts[active]); close(); break;
        default:
          if (multi && (e.ctrlKey || e.metaKey) && k.toLowerCase() === 'a') { e.preventDefault(); setAll(); render(); changed(); }
          else if (printable) { e.preventDefault(); typeahead(k); }
      }
    });
    combo.addEventListener('click', () => (isOpen() ? close() : open(current())));
    combo.addEventListener('blur', () => { if (isOpen()) close(); });
    label.addEventListener('click', e => { e.preventDefault(); combo.focus(); });
    popup.addEventListener('mousedown', e => e.preventDefault()); // keep focus on the combobox
    list.addEventListener('click', e => {
      const o = e.target.closest('[role="option"]');
      if (!o) return;
      setActive(opts.indexOf(o));
      pick(o);
      if (!multi) close();
      combo.focus();
    });
    list.addEventListener('pointermove', e => {
      const o = e.target.closest('[role="option"]');
      if (o && !o.hasAttribute('data-active')) setActive(opts.indexOf(o));
    });
    document.addEventListener('pointerdown', e => { if (isOpen() && !root.contains(e.target)) close(); });
    if (clear) clear.addEventListener('click', () => {
      if (multi) [...native.options].forEach(n => { n.selected = false; });
      else native.value = '';
      render();
      changed();
      combo.focus();
    });
    if (native.form) native.form.addEventListener('reset', () => setTimeout(render));
    render();
  });
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Counts, badge and a clear button</span><div class="card demo-card"><div class="card-body">
<div class="ms" data-select data-names="9">
  <div class="d-flex align-items-baseline justify-content-between gap-3"><label class="form-label" id="ms-am-label" for="ms-am-native">Amenities</label><button type="button" class="btn btn-link ms-clear" data-clear aria-label="Clear amenities" hidden>Clear</button></div>
  <select class="form-select ms-native" id="ms-am-native" name="amenities" multiple size="8">
    <option value="wifi" selected>Wi-Fi</option><option value="parking">Free parking</option><option value="kitchen" selected>Kitchen</option><option value="washer">Washing machine</option>
    <option value="ac" selected>Air conditioning</option><option value="pool">Pool</option><option value="ev">EV charger</option><option value="pets">Pets allowed</option>
  </select>
  <div class="position-relative" data-ui hidden>
    <div class="form-select ms-combo d-flex align-items-center" id="ms-am-combo" role="combobox" tabindex="0" aria-haspopup="listbox" aria-expanded="false" aria-controls="ms-am-list" aria-labelledby="ms-am-label">
      <svg class="ms-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 10.5 12 3l9 7.5"/><path d="M5 9v11h14V9"/><path d="M10 20v-6h4v6"/></svg>
      <span class="ms-shown flex-grow-1 text-truncate" data-shown data-placeholder="Any amenities">Any amenities</span>
      <span class="badge rounded-pill ms-count" data-count aria-hidden="true" hidden>0</span>
      <svg class="ms-chev" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m4 6 4 4 4-4"/></svg>
    </div>
    <div class="dropdown-menu ms-pop" data-popup hidden>
      <div class="ms-list" role="listbox" id="ms-am-list" aria-labelledby="ms-am-label" aria-multiselectable="true" tabindex="-1">
        <div class="dropdown-item ms-opt d-flex align-items-center" role="option" id="ms-am-1" data-value="wifi" data-label="Wi-Fi" aria-selected="false"><span class="form-check-input ms-box" aria-hidden="true"></span>Wi-Fi <span class="ms-num">128 stays</span></div>
        <div class="dropdown-item ms-opt d-flex align-items-center" role="option" id="ms-am-2" data-value="parking" data-label="Free parking" aria-selected="false"><span class="form-check-input ms-box" aria-hidden="true"></span>Free parking <span class="ms-num">64 stays</span></div>
        <div class="dropdown-item ms-opt d-flex align-items-center" role="option" id="ms-am-3" data-value="kitchen" data-label="Kitchen" aria-selected="false"><span class="form-check-input ms-box" aria-hidden="true"></span>Kitchen <span class="ms-num">91 stays</span></div>
        <div class="dropdown-item ms-opt d-flex align-items-center" role="option" id="ms-am-4" data-value="washer" data-label="Washing machine" aria-selected="false"><span class="form-check-input ms-box" aria-hidden="true"></span>Washing machine <span class="ms-num">47 stays</span></div>
        <div class="dropdown-item ms-opt d-flex align-items-center" role="option" id="ms-am-5" data-value="ac" data-label="Air conditioning" aria-selected="false"><span class="form-check-input ms-box" aria-hidden="true"></span>Air conditioning <span class="ms-num">73 stays</span></div>
        <div class="dropdown-item ms-opt d-flex align-items-center" role="option" id="ms-am-6" data-value="pool" data-label="Pool" aria-selected="false"><span class="form-check-input ms-box" aria-hidden="true"></span>Pool <span class="ms-num">12 stays</span></div>
        <div class="dropdown-item ms-opt d-flex align-items-center" role="option" id="ms-am-7" data-value="ev" data-label="EV charger" aria-selected="false"><span class="form-check-input ms-box" aria-hidden="true"></span>EV charger <span class="ms-num">9 stays</span></div>
        <div class="dropdown-item ms-opt d-flex align-items-center" role="option" id="ms-am-8" data-value="pets" data-label="Pets allowed" aria-selected="false"><span class="form-check-input ms-box" aria-hidden="true"></span>Pets allowed <span class="ms-num">38 stays</span></div>
      </div>
    </div>
  </div>
</div>
</div></div></div>

<div><span class="lbl">Checkbox dropdown with select all</span><div class="card demo-card"><div class="card-body">
<div class="ms" data-select>
  <label class="form-label" id="ms-status-label" for="ms-status-native">Status</label>
  <select class="form-select ms-native" id="ms-status-native" name="status" multiple size="6">
    <option value="backlog">Backlog</option><option value="todo">To do</option><option value="progress" selected>In progress</option>
    <option value="review" selected>In review</option><option value="done">Done</option><option value="cancelled">Cancelled</option>
  </select>
  <div class="position-relative" data-ui hidden>
    <div class="form-select ms-combo d-flex align-items-center" id="ms-status-combo" role="combobox" tabindex="0" aria-haspopup="listbox" aria-expanded="false" aria-controls="ms-status-list" aria-labelledby="ms-status-label">
      <span class="ms-shown flex-grow-1 text-truncate" data-shown data-placeholder="Any status" data-all-text="All statuses">Any status</span>
      <svg class="ms-chev" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m4 6 4 4 4-4"/></svg>
    </div>
    <div class="dropdown-menu ms-pop" data-popup hidden>
      <div class="ms-list" role="listbox" id="ms-status-list" aria-labelledby="ms-status-label" aria-multiselectable="true" tabindex="-1">
        <div class="dropdown-item ms-opt ms-all d-flex align-items-center" role="option" id="ms-status-all" data-all aria-selected="false"><span class="form-check-input ms-box" aria-hidden="true"></span>Select all</div>
        <div class="dropdown-item ms-opt d-flex align-items-center" role="option" id="ms-status-1" data-value="backlog" aria-selected="false"><span class="form-check-input ms-box" aria-hidden="true"></span><span class="ms-dot" style="--dot:#64748b" aria-hidden="true"></span>Backlog</div>
        <div class="dropdown-item ms-opt d-flex align-items-center" role="option" id="ms-status-2" data-value="todo" aria-selected="false"><span class="form-check-input ms-box" aria-hidden="true"></span><span class="ms-dot" style="--dot:#2563eb" aria-hidden="true"></span>To do</div>
        <div class="dropdown-item ms-opt d-flex align-items-center" role="option" id="ms-status-3" data-value="progress" aria-selected="false"><span class="form-check-input ms-box" aria-hidden="true"></span><span class="ms-dot" style="--dot:#d97706" aria-hidden="true"></span>In progress</div>
        <div class="dropdown-item ms-opt d-flex align-items-center" role="option" id="ms-status-4" data-value="review" aria-selected="false"><span class="form-check-input ms-box" aria-hidden="true"></span><span class="ms-dot" style="--dot:#7c3aed" aria-hidden="true"></span>In review</div>
        <div class="dropdown-item ms-opt d-flex align-items-center" role="option" id="ms-status-5" data-value="done" aria-selected="false"><span class="form-check-input ms-box" aria-hidden="true"></span><span class="ms-dot" style="--dot:#16a34a" aria-hidden="true"></span>Done</div>
        <div class="dropdown-item ms-opt d-flex align-items-center" role="option" id="ms-status-6" data-value="cancelled" aria-selected="false"><span class="form-check-input ms-box" aria-hidden="true"></span><span class="ms-dot" style="--dot:#dc2626" aria-hidden="true"></span>Cancelled</div>
      </div>
      <p class="ms-foot d-flex gap-3 mb-0" aria-hidden="true"><span><kbd>Space</kbd>tick</span><span><kbd>Ctrl A</kbd>all</span><span><kbd>Esc</kbd>close</span></p>
    </div>
  </div>
</div>
</div></div></div>
CSS
/* Multiselect dropdown with checkboxes on Bootstrap parts: a .form-select look for the select-only combobox
   (WAI-ARIA APG), a .dropdown-menu for the popup, .dropdown-item rows and .form-check-input boxes. The script sets
   aria-expanded, data-active and data-chosen; a hidden <select multiple> keeps the real form value. */
.ms{--ms-accent:#0f766e;--ms-tint:#e2f1ee;--ms-ring:rgba(15,118,110,.28);--ms-line:#859190;--ms-ink:#15202b;--ms-muted:#56615f;
  --ms-tick:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m3.5 8.5 3 3 6-7'/%3E%3C/svg%3E");
  --ms-dash:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-width='2.4' stroke-linecap='round'%3E%3Cpath d='M4 8h8'/%3E%3C/svg%3E");
  position:relative;display:grid;grid-template-columns:minmax(0,1fr);gap:7px;color:var(--ms-ink)}
.ms [hidden]{display:none!important} /* beats .d-flex and .badge display */
.ms .form-label{margin:0;font-size:.9rem;font-weight:600}
.ms-clear{--bs-btn-color:var(--ms-accent);--bs-btn-hover-color:var(--ms-accent);--bs-btn-active-color:var(--ms-accent);
  padding:2px 4px;margin:-2px -4px;font-size:.82rem;font-weight:600;text-decoration:none;border-radius:6px}
.ms-clear:hover{text-decoration:underline}
.ms-clear:focus-visible{box-shadow:none;outline:2px solid var(--ms-accent);outline-offset:1px}
.ms-native{--bs-form-select-bg-img:none;border-color:var(--ms-line);border-radius:10px;padding:6px}
.ms .ms-combo{--bs-form-select-bg-img:none;gap:10px;height:44px;padding:0 14px;font-size:.95rem;line-height:1.45;color:var(--ms-ink);border-color:var(--ms-line);
  border-radius:10px;box-shadow:0 1px 2px rgba(15,30,40,.06);cursor:pointer;user-select:none}
.ms .ms-combo:hover{border-color:#5b6766}
.ms .ms-combo:focus{border-color:var(--ms-line);box-shadow:0 1px 2px rgba(15,30,40,.06)}
.ms .ms-combo:focus-visible,.ms .ms-combo[aria-expanded="true"]{border-color:var(--ms-accent);box-shadow:0 0 0 4px var(--ms-ring)}
.ms-ico{flex:none;width:18px;height:18px;color:var(--ms-accent)}
.ms-combo[data-empty] .ms-shown{color:var(--ms-muted)}
.ms-count{min-width:22px;padding:.3em .6em;background:var(--ms-accent);font-size:.75rem}
.ms-chev{flex:none;width:16px;height:16px;color:var(--ms-muted);transition:transform .2s}
.ms-combo[aria-expanded="true"] .ms-chev{transform:rotate(180deg)}
.ms .ms-pop{--bs-dropdown-padding-x:6px;--bs-dropdown-padding-y:6px;--bs-dropdown-border-color:#d3dbd9;--bs-dropdown-border-radius:12px;
  --bs-dropdown-link-color:var(--ms-ink);--bs-dropdown-link-hover-color:var(--ms-ink);--bs-dropdown-link-hover-bg:transparent;
  --bs-dropdown-link-active-color:var(--ms-ink);--bs-dropdown-link-active-bg:transparent;
  display:block;left:0;right:0;top:calc(100% + 6px);z-index:30;box-shadow:0 18px 40px -12px rgba(15,30,40,.3)}
.ms-list{max-height:264px;overflow-y:auto;overscroll-behavior:contain;outline:none}
.ms .ms-opt{gap:11px;padding:8px 10px;border-radius:8px;font-size:.93rem;white-space:normal;cursor:pointer}
.ms .ms-opt[data-active]{background:var(--ms-tint)}
.ms .ms-box{flex:none;width:18px;height:18px;margin:0;border:1.5px solid #77817f;border-radius:5px;background-size:13px}
.ms-opt[data-chosen] .ms-box{--bs-form-check-bg-image:var(--ms-tick);background-color:var(--ms-accent);border-color:var(--ms-accent)}
.ms-opt[data-mixed] .ms-box{--bs-form-check-bg-image:var(--ms-dash);background-color:var(--ms-accent);border-color:var(--ms-accent)}
.ms .ms-all{position:relative;margin-bottom:7px;font-weight:600}
.ms-all::after{content:"";position:absolute;left:10px;right:10px;bottom:-4px;height:1px;background:#e0e6e5}
.ms-dot{flex:none;width:9px;height:9px;margin-left:-3px;border-radius:50%;background:var(--dot)}
.ms-num{margin-left:auto;font-size:.8rem;color:var(--ms-muted);font-variant-numeric:tabular-nums}
.ms-foot{padding:9px 10px 3px;margin-top:6px;border-top:1px solid #e0e6e5;font-size:.76rem;color:var(--ms-muted)}
.ms-foot kbd{padding:1px 6px;margin-right:4px;font:inherit;font-weight:600;color:var(--ms-ink);background:#f1f5f4;border:1px solid #ccd5d3;border-radius:5px}
@media (prefers-reduced-motion:reduce){.ms .ms-combo,.ms-chev{transition:none}}
JavaScript
// Select-only combobox with a listbox popup (WAI-ARIA APG), single or multi-select.
// Focus stays on the combobox; aria-activedescendant points at the highlighted option.
// Closed: Down, Up, Enter or Space open it, Home / End open at the first / last option, typing opens and jumps.
// Open: Down / Up / Home / End / Page Up / Page Down move; Enter or Space picks (ticks in a multiselect);
// Ctrl+A ticks all; typing jumps to the next match; Escape closes; Tab closes and moves on.
// The <select> inside each [data-select] holds the real form value and the starting selection. It is hidden
// once this script runs, so without JavaScript the native control is still there and still works.
(() => {
  document.querySelectorAll('[data-select]').forEach(root => {
    const native = root.querySelector('select');
    const label = root.querySelector('label');
    const combo = root.querySelector('[role="combobox"]');
    const popup = root.querySelector('[data-popup]');
    const list = root.querySelector('[role="listbox"]');
    const shown = root.querySelector('[data-shown]');
    const count = root.querySelector('[data-count]');
    const clear = root.querySelector('[data-clear]');
    const multi = native.multiple;
    const opts = [...list.querySelectorAll('[role="option"]')];
    const all = list.querySelector('[data-all]');
    const items = opts.filter(o => o !== all);
    const source = o => [...native.options].find(n => n.value === o.dataset.value);
    const text = o => o.dataset.label || o.textContent.trim();
    const isOpen = () => combo.getAttribute('aria-expanded') === 'true';
    const changed = () => native.dispatchEvent(new Event('change', { bubbles: true }));
    let active = -1, typed = '', typedAt = 0;

    native.hidden = true;
    root.querySelector('[data-ui]').hidden = false;

    function render() {
      const on = items.filter(o => source(o).selected);
      items.forEach(o => {
        o.toggleAttribute('data-chosen', on.includes(o));
        if (multi || !isOpen()) o.setAttribute('aria-selected', on.includes(o));
      });
      if (all) {
        all.setAttribute('aria-selected', on.length === items.length);
        all.toggleAttribute('data-chosen', on.length === items.length);
        all.toggleAttribute('data-mixed', on.length > 0 && on.length < items.length);
      }
      if (!on.length) shown.textContent = shown.dataset.placeholder;
      else if (!multi) {
        const pic = on[0].querySelector('[data-pic]');
        shown.replaceChildren(...(pic ? [pic.cloneNode(true)] : []), text(on[0]));
      } else if (on.length === items.length && shown.dataset.allText) shown.textContent = shown.dataset.allText;
      else shown.textContent = on.length <= (+root.dataset.names || 2) ? on.map(text).join(', ') : on.length + ' selected';
      combo.toggleAttribute('data-empty', !on.length);
      if (count) { count.textContent = on.length; count.hidden = !on.length; }
      if (clear) clear.hidden = !on.length;
    }
    function setActive(i) {
      active = i;
      opts.forEach((o, n) => {
        o.toggleAttribute('data-active', n === i);
        if (!multi && i > -1) o.setAttribute('aria-selected', n === i); // single select: selection follows the highlight
      });
      if (i < 0) return combo.removeAttribute('aria-activedescendant');
      combo.setAttribute('aria-activedescendant', opts[i].id);
      const o = opts[i].getBoundingClientRect(), b = list.getBoundingClientRect(); // keep it in view without scrolling the page
      if (i === 0) list.scrollTop = 0;
      else if (o.top < b.top) list.scrollTop -= b.top - o.top;
      else if (o.bottom > b.bottom) list.scrollTop += o.bottom - b.bottom;
    }
    function open(i) {
      if (!isOpen()) { popup.hidden = false; combo.setAttribute('aria-expanded', 'true'); }
      if (i !== undefined) setActive(Math.max(0, Math.min(opts.length - 1, i)));
    }
    function close() {
      popup.hidden = true;
      combo.setAttribute('aria-expanded', 'false');
      setActive(-1);
      render();
    }
    const current = () => Math.max(0, opts.findIndex(o => o.hasAttribute('data-chosen')));
    function setAll() {
      const every = items.every(o => source(o).selected);
      items.forEach(o => { source(o).selected = !every; });
    }
    function pick(o) {
      if (!multi) native.value = o.dataset.value;
      else if (o === all) setAll();
      else source(o).selected = !source(o).selected;
      render();
      changed();
    }
    function typeahead(ch) {
      const now = Date.now();
      typed = (now - typedAt > 600 ? '' : typed) + ch.toLowerCase();
      typedAt = now;
      const same = [...typed].every(c => c === typed[0]); // pressing one letter again cycles through its matches
      const q = same ? typed[0] : typed;
      const from = active < 0 ? 0 : same ? active + 1 : active;
      for (let k = 0; k < opts.length; k++) {
        const n = (from + k) % opts.length;
        if (text(opts[n]).toLowerCase().startsWith(q)) return open(n);
      }
    }
    const typing = () => typed && Date.now() - typedAt < 600 && opts.some(o => text(o).toLowerCase().startsWith(typed + ' '));

    combo.addEventListener('keydown', e => {
      const k = e.key, last = opts.length - 1;
      const printable = k.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey;
      if (k.length > 1) typed = ''; // arrows, Home, End… end a type-ahead run
      if (!isOpen()) {
        if (k === 'ArrowDown' || k === 'ArrowUp' || k === 'Enter' || k === ' ') { e.preventDefault(); open(e.altKey ? undefined : current()); }
        else if (k === 'Home') { e.preventDefault(); open(0); }
        else if (k === 'End') { e.preventDefault(); open(last); }
        else if (printable) { e.preventDefault(); open(); typeahead(k); }
        return;
      }
      if (k === ' ' && typing()) { e.preventDefault(); return typeahead(k); } // a space inside a name, as in "In review"
      switch (k) {
        case 'ArrowDown': e.preventDefault(); setActive(Math.min(active + 1, last)); break;
        case 'ArrowUp':
          e.preventDefault();
          if (e.altKey) { if (!multi && active > -1) pick(opts[active]); close(); }
          else setActive(Math.max(active - 1, 0));
          break;
        case 'Home': e.preventDefault(); setActive(0); break;
        case 'End': e.preventDefault(); setActive(last); break;
        case 'PageDown': e.preventDefault(); setActive(Math.min(active + 10, last)); break;
        case 'PageUp': e.preventDefault(); setActive(Math.max(active - 10, 0)); break;
        case 'Enter': case ' ':
          e.preventDefault();
          typed = '';
          if (active > -1) pick(opts[active]);
          if (!multi) close();
          break;
        case 'Escape': e.preventDefault(); close(); break;
        case 'Tab': if (!multi && active > -1) pick(opts[active]); close(); break;
        default:
          if (multi && (e.ctrlKey || e.metaKey) && k.toLowerCase() === 'a') { e.preventDefault(); setAll(); render(); changed(); }
          else if (printable) { e.preventDefault(); typeahead(k); }
      }
    });
    combo.addEventListener('click', () => (isOpen() ? close() : open(current())));
    combo.addEventListener('blur', () => { if (isOpen()) close(); });
    label.addEventListener('click', e => { e.preventDefault(); combo.focus(); });
    popup.addEventListener('mousedown', e => e.preventDefault()); // keep focus on the combobox
    list.addEventListener('click', e => {
      const o = e.target.closest('[role="option"]');
      if (!o) return;
      setActive(opts.indexOf(o));
      pick(o);
      if (!multi) close();
      combo.focus();
    });
    list.addEventListener('pointermove', e => {
      const o = e.target.closest('[role="option"]');
      if (o && !o.hasAttribute('data-active')) setActive(opts.indexOf(o));
    });
    document.addEventListener('pointerdown', e => { if (isOpen() && !root.contains(e.target)) close(); });
    if (clear) clear.addEventListener('click', () => {
      if (multi) [...native.options].forEach(n => { n.selected = false; });
      else native.value = '';
      render();
      changed();
      combo.focus();
    });
    if (native.form) native.form.addEventListener('reset', () => setTimeout(render));
    render();
  });
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#56615f] uppercase">Counts, badge and a clear button</span><div class="rounded-2xl border border-[#dfe7e5] bg-white px-6 pt-[22px] pb-6 shadow-[0_1px_2px_rgba(15,30,40,.04)]">
<div class="relative grid grid-cols-[minmax(0,1fr)] gap-[7px]" data-select data-names="9">
  <div class="flex items-baseline justify-between gap-3"><label class="text-[.9rem] font-semibold" id="ms-am-label" for="ms-am-native">Amenities</label><button type="button" class="-mx-1 -my-0.5 cursor-pointer rounded-md px-1 py-0.5 text-[.82rem] font-semibold text-ms-accent hover:underline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ms-accent" data-clear aria-label="Clear amenities" hidden>Clear</button></div>
  <select class="w-full rounded-[10px] border border-ms-line bg-white p-1.5" id="ms-am-native" name="amenities" multiple size="8">
    <option value="wifi" selected>Wi-Fi</option><option value="parking">Free parking</option><option value="kitchen" selected>Kitchen</option><option value="washer">Washing machine</option>
    <option value="ac" selected>Air conditioning</option><option value="pool">Pool</option><option value="ev">EV charger</option><option value="pets">Pets allowed</option>
  </select>
  <div class="relative" data-ui hidden>
    <div class="group flex h-11 cursor-pointer items-center gap-2.5 rounded-[10px] border border-ms-line bg-white px-3.5 text-[.95rem] shadow-[0_1px_2px_rgba(15,30,40,.06)] select-none transition-[border-color,box-shadow] duration-150 hover:border-[#5b6766] focus-visible:border-ms-accent focus-visible:shadow-[0_0_0_4px_rgba(15,118,110,.28)] focus-visible:outline-none aria-expanded:border-ms-accent aria-expanded:shadow-[0_0_0_4px_rgba(15,118,110,.28)] motion-reduce:transition-none" id="ms-am-combo" role="combobox" tabindex="0" aria-haspopup="listbox" aria-expanded="false" aria-controls="ms-am-list" aria-labelledby="ms-am-label">
      <svg class="size-[18px] flex-none text-ms-accent" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 10.5 12 3l9 7.5"/><path d="M5 9v11h14V9"/><path d="M10 20v-6h4v6"/></svg>
      <span class="min-w-0 flex-1 truncate group-data-empty:text-ms-muted" data-shown data-placeholder="Any amenities">Any amenities</span>
      <span class="grid h-[22px] min-w-[22px] flex-none place-items-center rounded-full bg-ms-accent px-[7px] text-[.75rem] font-bold text-white" data-count aria-hidden="true" hidden>0</span>
      <svg class="size-4 flex-none text-ms-muted transition-transform duration-200 group-aria-expanded:rotate-180 motion-reduce:transition-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m4 6 4 4 4-4"/></svg>
    </div>
    <div class="absolute top-[calc(100%+6px)] right-0 left-0 z-30 rounded-xl border border-[#d3dbd9] bg-white p-1.5 shadow-[0_18px_40px_-12px_rgba(15,30,40,.3)]" data-popup hidden>
      <div class="max-h-[264px] overflow-y-auto overscroll-contain outline-none" role="listbox" id="ms-am-list" aria-labelledby="ms-am-label" aria-multiselectable="true" tabindex="-1">
        <div class="group flex cursor-pointer items-center gap-[11px] rounded-lg px-2.5 py-2 text-[.93rem] data-active:bg-ms-tint" role="option" id="ms-am-1" data-value="wifi" data-label="Wi-Fi" aria-selected="false"><span class="size-[18px] flex-none rounded-[5px] border-[1.5px] border-[#77817f] bg-white bg-size-[13px] bg-center bg-no-repeat group-data-chosen:ms-tick group-data-chosen:border-ms-accent group-data-chosen:bg-ms-accent group-data-mixed:ms-dash group-data-mixed:border-ms-accent group-data-mixed:bg-ms-accent" aria-hidden="true"></span>Wi-Fi <span class="ml-auto text-[.8rem] text-ms-muted tabular-nums">128 stays</span></div>
        <div class="group flex cursor-pointer items-center gap-[11px] rounded-lg px-2.5 py-2 text-[.93rem] data-active:bg-ms-tint" role="option" id="ms-am-2" data-value="parking" data-label="Free parking" aria-selected="false"><span class="size-[18px] flex-none rounded-[5px] border-[1.5px] border-[#77817f] bg-white bg-size-[13px] bg-center bg-no-repeat group-data-chosen:ms-tick group-data-chosen:border-ms-accent group-data-chosen:bg-ms-accent group-data-mixed:ms-dash group-data-mixed:border-ms-accent group-data-mixed:bg-ms-accent" aria-hidden="true"></span>Free parking <span class="ml-auto text-[.8rem] text-ms-muted tabular-nums">64 stays</span></div>
        <div class="group flex cursor-pointer items-center gap-[11px] rounded-lg px-2.5 py-2 text-[.93rem] data-active:bg-ms-tint" role="option" id="ms-am-3" data-value="kitchen" data-label="Kitchen" aria-selected="false"><span class="size-[18px] flex-none rounded-[5px] border-[1.5px] border-[#77817f] bg-white bg-size-[13px] bg-center bg-no-repeat group-data-chosen:ms-tick group-data-chosen:border-ms-accent group-data-chosen:bg-ms-accent group-data-mixed:ms-dash group-data-mixed:border-ms-accent group-data-mixed:bg-ms-accent" aria-hidden="true"></span>Kitchen <span class="ml-auto text-[.8rem] text-ms-muted tabular-nums">91 stays</span></div>
        <div class="group flex cursor-pointer items-center gap-[11px] rounded-lg px-2.5 py-2 text-[.93rem] data-active:bg-ms-tint" role="option" id="ms-am-4" data-value="washer" data-label="Washing machine" aria-selected="false"><span class="size-[18px] flex-none rounded-[5px] border-[1.5px] border-[#77817f] bg-white bg-size-[13px] bg-center bg-no-repeat group-data-chosen:ms-tick group-data-chosen:border-ms-accent group-data-chosen:bg-ms-accent group-data-mixed:ms-dash group-data-mixed:border-ms-accent group-data-mixed:bg-ms-accent" aria-hidden="true"></span>Washing machine <span class="ml-auto text-[.8rem] text-ms-muted tabular-nums">47 stays</span></div>
        <div class="group flex cursor-pointer items-center gap-[11px] rounded-lg px-2.5 py-2 text-[.93rem] data-active:bg-ms-tint" role="option" id="ms-am-5" data-value="ac" data-label="Air conditioning" aria-selected="false"><span class="size-[18px] flex-none rounded-[5px] border-[1.5px] border-[#77817f] bg-white bg-size-[13px] bg-center bg-no-repeat group-data-chosen:ms-tick group-data-chosen:border-ms-accent group-data-chosen:bg-ms-accent group-data-mixed:ms-dash group-data-mixed:border-ms-accent group-data-mixed:bg-ms-accent" aria-hidden="true"></span>Air conditioning <span class="ml-auto text-[.8rem] text-ms-muted tabular-nums">73 stays</span></div>
        <div class="group flex cursor-pointer items-center gap-[11px] rounded-lg px-2.5 py-2 text-[.93rem] data-active:bg-ms-tint" role="option" id="ms-am-6" data-value="pool" data-label="Pool" aria-selected="false"><span class="size-[18px] flex-none rounded-[5px] border-[1.5px] border-[#77817f] bg-white bg-size-[13px] bg-center bg-no-repeat group-data-chosen:ms-tick group-data-chosen:border-ms-accent group-data-chosen:bg-ms-accent group-data-mixed:ms-dash group-data-mixed:border-ms-accent group-data-mixed:bg-ms-accent" aria-hidden="true"></span>Pool <span class="ml-auto text-[.8rem] text-ms-muted tabular-nums">12 stays</span></div>
        <div class="group flex cursor-pointer items-center gap-[11px] rounded-lg px-2.5 py-2 text-[.93rem] data-active:bg-ms-tint" role="option" id="ms-am-7" data-value="ev" data-label="EV charger" aria-selected="false"><span class="size-[18px] flex-none rounded-[5px] border-[1.5px] border-[#77817f] bg-white bg-size-[13px] bg-center bg-no-repeat group-data-chosen:ms-tick group-data-chosen:border-ms-accent group-data-chosen:bg-ms-accent group-data-mixed:ms-dash group-data-mixed:border-ms-accent group-data-mixed:bg-ms-accent" aria-hidden="true"></span>EV charger <span class="ml-auto text-[.8rem] text-ms-muted tabular-nums">9 stays</span></div>
        <div class="group flex cursor-pointer items-center gap-[11px] rounded-lg px-2.5 py-2 text-[.93rem] data-active:bg-ms-tint" role="option" id="ms-am-8" data-value="pets" data-label="Pets allowed" aria-selected="false"><span class="size-[18px] flex-none rounded-[5px] border-[1.5px] border-[#77817f] bg-white bg-size-[13px] bg-center bg-no-repeat group-data-chosen:ms-tick group-data-chosen:border-ms-accent group-data-chosen:bg-ms-accent group-data-mixed:ms-dash group-data-mixed:border-ms-accent group-data-mixed:bg-ms-accent" aria-hidden="true"></span>Pets allowed <span class="ml-auto text-[.8rem] text-ms-muted tabular-nums">38 stays</span></div>
      </div>
    </div>
  </div>
</div>
</div></div>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#56615f] uppercase">Checkbox dropdown with select all</span><div class="rounded-2xl border border-[#dfe7e5] bg-white px-6 pt-[22px] pb-6 shadow-[0_1px_2px_rgba(15,30,40,.04)]">
<div class="relative grid grid-cols-[minmax(0,1fr)] gap-[7px]" data-select>
  <label class="text-[.9rem] font-semibold" id="ms-status-label" for="ms-status-native">Status</label>
  <select class="w-full rounded-[10px] border border-ms-line bg-white p-1.5" id="ms-status-native" name="status" multiple size="6">
    <option value="backlog">Backlog</option><option value="todo">To do</option><option value="progress" selected>In progress</option>
    <option value="review" selected>In review</option><option value="done">Done</option><option value="cancelled">Cancelled</option>
  </select>
  <div class="relative" data-ui hidden>
    <div class="group flex h-11 cursor-pointer items-center gap-2.5 rounded-[10px] border border-ms-line bg-white px-3.5 text-[.95rem] shadow-[0_1px_2px_rgba(15,30,40,.06)] select-none transition-[border-color,box-shadow] duration-150 hover:border-[#5b6766] focus-visible:border-ms-accent focus-visible:shadow-[0_0_0_4px_rgba(15,118,110,.28)] focus-visible:outline-none aria-expanded:border-ms-accent aria-expanded:shadow-[0_0_0_4px_rgba(15,118,110,.28)] motion-reduce:transition-none" id="ms-status-combo" role="combobox" tabindex="0" aria-haspopup="listbox" aria-expanded="false" aria-controls="ms-status-list" aria-labelledby="ms-status-label">
      <span class="min-w-0 flex-1 truncate group-data-empty:text-ms-muted" data-shown data-placeholder="Any status" data-all-text="All statuses">Any status</span>
      <svg class="size-4 flex-none text-ms-muted transition-transform duration-200 group-aria-expanded:rotate-180 motion-reduce:transition-none" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m4 6 4 4 4-4"/></svg>
    </div>
    <div class="absolute top-[calc(100%+6px)] right-0 left-0 z-30 rounded-xl border border-[#d3dbd9] bg-white p-1.5 shadow-[0_18px_40px_-12px_rgba(15,30,40,.3)]" data-popup hidden>
      <div class="max-h-[264px] overflow-y-auto overscroll-contain outline-none" role="listbox" id="ms-status-list" aria-labelledby="ms-status-label" aria-multiselectable="true" tabindex="-1">
        <div class="group flex cursor-pointer items-center gap-[11px] rounded-lg px-2.5 py-2 text-[.93rem] data-active:bg-ms-tint relative mb-[7px] font-semibold after:absolute after:inset-x-2.5 after:-bottom-1 after:h-px after:bg-[#e0e6e5]" role="option" id="ms-status-all" data-all aria-selected="false"><span class="size-[18px] flex-none rounded-[5px] border-[1.5px] border-[#77817f] bg-white bg-size-[13px] bg-center bg-no-repeat group-data-chosen:ms-tick group-data-chosen:border-ms-accent group-data-chosen:bg-ms-accent group-data-mixed:ms-dash group-data-mixed:border-ms-accent group-data-mixed:bg-ms-accent" aria-hidden="true"></span>Select all</div>
        <div class="group flex cursor-pointer items-center gap-[11px] rounded-lg px-2.5 py-2 text-[.93rem] data-active:bg-ms-tint" role="option" id="ms-status-1" data-value="backlog" aria-selected="false"><span class="size-[18px] flex-none rounded-[5px] border-[1.5px] border-[#77817f] bg-white bg-size-[13px] bg-center bg-no-repeat group-data-chosen:ms-tick group-data-chosen:border-ms-accent group-data-chosen:bg-ms-accent group-data-mixed:ms-dash group-data-mixed:border-ms-accent group-data-mixed:bg-ms-accent" aria-hidden="true"></span><span class="-ml-[3px] size-[9px] flex-none rounded-full bg-(--dot)" style="--dot:#64748b" aria-hidden="true"></span>Backlog</div>
        <div class="group flex cursor-pointer items-center gap-[11px] rounded-lg px-2.5 py-2 text-[.93rem] data-active:bg-ms-tint" role="option" id="ms-status-2" data-value="todo" aria-selected="false"><span class="size-[18px] flex-none rounded-[5px] border-[1.5px] border-[#77817f] bg-white bg-size-[13px] bg-center bg-no-repeat group-data-chosen:ms-tick group-data-chosen:border-ms-accent group-data-chosen:bg-ms-accent group-data-mixed:ms-dash group-data-mixed:border-ms-accent group-data-mixed:bg-ms-accent" aria-hidden="true"></span><span class="-ml-[3px] size-[9px] flex-none rounded-full bg-(--dot)" style="--dot:#2563eb" aria-hidden="true"></span>To do</div>
        <div class="group flex cursor-pointer items-center gap-[11px] rounded-lg px-2.5 py-2 text-[.93rem] data-active:bg-ms-tint" role="option" id="ms-status-3" data-value="progress" aria-selected="false"><span class="size-[18px] flex-none rounded-[5px] border-[1.5px] border-[#77817f] bg-white bg-size-[13px] bg-center bg-no-repeat group-data-chosen:ms-tick group-data-chosen:border-ms-accent group-data-chosen:bg-ms-accent group-data-mixed:ms-dash group-data-mixed:border-ms-accent group-data-mixed:bg-ms-accent" aria-hidden="true"></span><span class="-ml-[3px] size-[9px] flex-none rounded-full bg-(--dot)" style="--dot:#d97706" aria-hidden="true"></span>In progress</div>
        <div class="group flex cursor-pointer items-center gap-[11px] rounded-lg px-2.5 py-2 text-[.93rem] data-active:bg-ms-tint" role="option" id="ms-status-4" data-value="review" aria-selected="false"><span class="size-[18px] flex-none rounded-[5px] border-[1.5px] border-[#77817f] bg-white bg-size-[13px] bg-center bg-no-repeat group-data-chosen:ms-tick group-data-chosen:border-ms-accent group-data-chosen:bg-ms-accent group-data-mixed:ms-dash group-data-mixed:border-ms-accent group-data-mixed:bg-ms-accent" aria-hidden="true"></span><span class="-ml-[3px] size-[9px] flex-none rounded-full bg-(--dot)" style="--dot:#7c3aed" aria-hidden="true"></span>In review</div>
        <div class="group flex cursor-pointer items-center gap-[11px] rounded-lg px-2.5 py-2 text-[.93rem] data-active:bg-ms-tint" role="option" id="ms-status-5" data-value="done" aria-selected="false"><span class="size-[18px] flex-none rounded-[5px] border-[1.5px] border-[#77817f] bg-white bg-size-[13px] bg-center bg-no-repeat group-data-chosen:ms-tick group-data-chosen:border-ms-accent group-data-chosen:bg-ms-accent group-data-mixed:ms-dash group-data-mixed:border-ms-accent group-data-mixed:bg-ms-accent" aria-hidden="true"></span><span class="-ml-[3px] size-[9px] flex-none rounded-full bg-(--dot)" style="--dot:#16a34a" aria-hidden="true"></span>Done</div>
        <div class="group flex cursor-pointer items-center gap-[11px] rounded-lg px-2.5 py-2 text-[.93rem] data-active:bg-ms-tint" role="option" id="ms-status-6" data-value="cancelled" aria-selected="false"><span class="size-[18px] flex-none rounded-[5px] border-[1.5px] border-[#77817f] bg-white bg-size-[13px] bg-center bg-no-repeat group-data-chosen:ms-tick group-data-chosen:border-ms-accent group-data-chosen:bg-ms-accent group-data-mixed:ms-dash group-data-mixed:border-ms-accent group-data-mixed:bg-ms-accent" aria-hidden="true"></span><span class="-ml-[3px] size-[9px] flex-none rounded-full bg-(--dot)" style="--dot:#dc2626" aria-hidden="true"></span>Cancelled</div>
      </div>
      <p class="mt-1.5 flex gap-4 border-t border-[#e0e6e5] px-2.5 pt-[9px] pb-[3px] text-[.76rem] text-ms-muted" aria-hidden="true"><span><kbd class="mr-1 rounded-[5px] border border-[#ccd5d3] bg-[#f1f5f4] px-1.5 py-px [font-family:inherit] font-semibold text-ms-ink">Space</kbd>tick</span><span><kbd class="mr-1 rounded-[5px] border border-[#ccd5d3] bg-[#f1f5f4] px-1.5 py-px [font-family:inherit] font-semibold text-ms-ink">Ctrl A</kbd>all</span><span><kbd class="mr-1 rounded-[5px] border border-[#ccd5d3] bg-[#f1f5f4] px-1.5 py-px [font-family:inherit] font-semibold text-ms-ink">Esc</kbd>close</span></p>
    </div>
  </div>
</div>
</div></div>
CSS
@theme {
  --color-ms-accent: #0f766e;
  --color-ms-tint: #e2f1ee;
  --color-ms-line: #859190;
  --color-ms-ink: #15202b;
  --color-ms-muted: #56615f;
}
/* the tick and the dash drawn inside a chosen / partly chosen checkbox */
@utility ms-tick { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m3.5 8.5 3 3 6-7'/%3E%3C/svg%3E"); }
@utility ms-dash { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-width='2.4' stroke-linecap='round'%3E%3Cpath d='M4 8h8'/%3E%3C/svg%3E"); }
JavaScript
// Select-only combobox with a listbox popup (WAI-ARIA APG), single or multi-select.
// Focus stays on the combobox; aria-activedescendant points at the highlighted option.
// Closed: Down, Up, Enter or Space open it, Home / End open at the first / last option, typing opens and jumps.
// Open: Down / Up / Home / End / Page Up / Page Down move; Enter or Space picks (ticks in a multiselect);
// Ctrl+A ticks all; typing jumps to the next match; Escape closes; Tab closes and moves on.
// The <select> inside each [data-select] holds the real form value and the starting selection. It is hidden
// once this script runs, so without JavaScript the native control is still there and still works.
(() => {
  document.querySelectorAll('[data-select]').forEach(root => {
    const native = root.querySelector('select');
    const label = root.querySelector('label');
    const combo = root.querySelector('[role="combobox"]');
    const popup = root.querySelector('[data-popup]');
    const list = root.querySelector('[role="listbox"]');
    const shown = root.querySelector('[data-shown]');
    const count = root.querySelector('[data-count]');
    const clear = root.querySelector('[data-clear]');
    const multi = native.multiple;
    const opts = [...list.querySelectorAll('[role="option"]')];
    const all = list.querySelector('[data-all]');
    const items = opts.filter(o => o !== all);
    const source = o => [...native.options].find(n => n.value === o.dataset.value);
    const text = o => o.dataset.label || o.textContent.trim();
    const isOpen = () => combo.getAttribute('aria-expanded') === 'true';
    const changed = () => native.dispatchEvent(new Event('change', { bubbles: true }));
    let active = -1, typed = '', typedAt = 0;

    native.hidden = true;
    root.querySelector('[data-ui]').hidden = false;

    function render() {
      const on = items.filter(o => source(o).selected);
      items.forEach(o => {
        o.toggleAttribute('data-chosen', on.includes(o));
        if (multi || !isOpen()) o.setAttribute('aria-selected', on.includes(o));
      });
      if (all) {
        all.setAttribute('aria-selected', on.length === items.length);
        all.toggleAttribute('data-chosen', on.length === items.length);
        all.toggleAttribute('data-mixed', on.length > 0 && on.length < items.length);
      }
      if (!on.length) shown.textContent = shown.dataset.placeholder;
      else if (!multi) {
        const pic = on[0].querySelector('[data-pic]');
        shown.replaceChildren(...(pic ? [pic.cloneNode(true)] : []), text(on[0]));
      } else if (on.length === items.length && shown.dataset.allText) shown.textContent = shown.dataset.allText;
      else shown.textContent = on.length <= (+root.dataset.names || 2) ? on.map(text).join(', ') : on.length + ' selected';
      combo.toggleAttribute('data-empty', !on.length);
      if (count) { count.textContent = on.length; count.hidden = !on.length; }
      if (clear) clear.hidden = !on.length;
    }
    function setActive(i) {
      active = i;
      opts.forEach((o, n) => {
        o.toggleAttribute('data-active', n === i);
        if (!multi && i > -1) o.setAttribute('aria-selected', n === i); // single select: selection follows the highlight
      });
      if (i < 0) return combo.removeAttribute('aria-activedescendant');
      combo.setAttribute('aria-activedescendant', opts[i].id);
      const o = opts[i].getBoundingClientRect(), b = list.getBoundingClientRect(); // keep it in view without scrolling the page
      if (i === 0) list.scrollTop = 0;
      else if (o.top < b.top) list.scrollTop -= b.top - o.top;
      else if (o.bottom > b.bottom) list.scrollTop += o.bottom - b.bottom;
    }
    function open(i) {
      if (!isOpen()) { popup.hidden = false; combo.setAttribute('aria-expanded', 'true'); }
      if (i !== undefined) setActive(Math.max(0, Math.min(opts.length - 1, i)));
    }
    function close() {
      popup.hidden = true;
      combo.setAttribute('aria-expanded', 'false');
      setActive(-1);
      render();
    }
    const current = () => Math.max(0, opts.findIndex(o => o.hasAttribute('data-chosen')));
    function setAll() {
      const every = items.every(o => source(o).selected);
      items.forEach(o => { source(o).selected = !every; });
    }
    function pick(o) {
      if (!multi) native.value = o.dataset.value;
      else if (o === all) setAll();
      else source(o).selected = !source(o).selected;
      render();
      changed();
    }
    function typeahead(ch) {
      const now = Date.now();
      typed = (now - typedAt > 600 ? '' : typed) + ch.toLowerCase();
      typedAt = now;
      const same = [...typed].every(c => c === typed[0]); // pressing one letter again cycles through its matches
      const q = same ? typed[0] : typed;
      const from = active < 0 ? 0 : same ? active + 1 : active;
      for (let k = 0; k < opts.length; k++) {
        const n = (from + k) % opts.length;
        if (text(opts[n]).toLowerCase().startsWith(q)) return open(n);
      }
    }
    const typing = () => typed && Date.now() - typedAt < 600 && opts.some(o => text(o).toLowerCase().startsWith(typed + ' '));

    combo.addEventListener('keydown', e => {
      const k = e.key, last = opts.length - 1;
      const printable = k.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey;
      if (k.length > 1) typed = ''; // arrows, Home, End… end a type-ahead run
      if (!isOpen()) {
        if (k === 'ArrowDown' || k === 'ArrowUp' || k === 'Enter' || k === ' ') { e.preventDefault(); open(e.altKey ? undefined : current()); }
        else if (k === 'Home') { e.preventDefault(); open(0); }
        else if (k === 'End') { e.preventDefault(); open(last); }
        else if (printable) { e.preventDefault(); open(); typeahead(k); }
        return;
      }
      if (k === ' ' && typing()) { e.preventDefault(); return typeahead(k); } // a space inside a name, as in "In review"
      switch (k) {
        case 'ArrowDown': e.preventDefault(); setActive(Math.min(active + 1, last)); break;
        case 'ArrowUp':
          e.preventDefault();
          if (e.altKey) { if (!multi && active > -1) pick(opts[active]); close(); }
          else setActive(Math.max(active - 1, 0));
          break;
        case 'Home': e.preventDefault(); setActive(0); break;
        case 'End': e.preventDefault(); setActive(last); break;
        case 'PageDown': e.preventDefault(); setActive(Math.min(active + 10, last)); break;
        case 'PageUp': e.preventDefault(); setActive(Math.max(active - 10, 0)); break;
        case 'Enter': case ' ':
          e.preventDefault();
          typed = '';
          if (active > -1) pick(opts[active]);
          if (!multi) close();
          break;
        case 'Escape': e.preventDefault(); close(); break;
        case 'Tab': if (!multi && active > -1) pick(opts[active]); close(); break;
        default:
          if (multi && (e.ctrlKey || e.metaKey) && k.toLowerCase() === 'a') { e.preventDefault(); setAll(); render(); changed(); }
          else if (printable) { e.preventDefault(); typeahead(k); }
      }
    });
    combo.addEventListener('click', () => (isOpen() ? close() : open(current())));
    combo.addEventListener('blur', () => { if (isOpen()) close(); });
    label.addEventListener('click', e => { e.preventDefault(); combo.focus(); });
    popup.addEventListener('mousedown', e => e.preventDefault()); // keep focus on the combobox
    list.addEventListener('click', e => {
      const o = e.target.closest('[role="option"]');
      if (!o) return;
      setActive(opts.indexOf(o));
      pick(o);
      if (!multi) close();
      combo.focus();
    });
    list.addEventListener('pointermove', e => {
      const o = e.target.closest('[role="option"]');
      if (o && !o.hasAttribute('data-active')) setActive(opts.indexOf(o));
    });
    document.addEventListener('pointerdown', e => { if (isOpen() && !root.contains(e.target)) close(); });
    if (clear) clear.addEventListener('click', () => {
      if (multi) [...native.options].forEach(n => { n.selected = false; });
      else native.value = '';
      render();
      changed();
      combo.focus();
    });
    if (native.form) native.form.addEventListener('reset', () => setTimeout(render));
    render();
  });
})();

About this set

The multiselect dropdown most filter panels need: a closed field that summarises the choice and opens a list of checkboxes. Counts, badge and a clear button is an amenities filter that lists the picked names, shows a count badge, gives each option its number of matching stays and offers a Clear button beside the label. Checkbox dropdown with select all is a task status filter with coloured dots, a Select all row that shows a dash when only some statuses are ticked, and a key hint footer; with one or two picks the field names them, with more it reads 3 selected, and with everything ticked it says All statuses. Both follow the WAI-ARIA select-only combobox pattern: focus stays on the field, aria-activedescendant marks the highlighted row, arrow keys, Home, End and Page keys move, Space or Enter ticks, Ctrl+A ticks or clears everything, typing jumps to a match, and Escape closes. A hidden select multiple holds the real value, so the form submits normally, and without JavaScript that native list is shown instead.

What’s included

  • Select all row with a mixed state
  • Summary text: names, then N selected, then All
  • Count badge and a Clear button
  • WAI-ARIA select-only combobox with a multi-select listbox and type-ahead
  • Real select multiple kept in sync for form submission
  • Native list shown when JavaScript is off

Accessibility

The field is a role="combobox" labelled by the visible label, with aria-expanded, aria-controls and aria-activedescendant pointing into a listbox marked aria-multiselectable; each option reports aria-selected. Arrow keys, Home, End, Page Up and Page Down move, Space or Enter ticks, Ctrl+A ticks or clears all, typing jumps to a match, and Escape or Tab closes. Focus shows a four-pixel ring, the ticks do not rely on colour alone, and the chevron turn and border transitions stop under prefers-reduced-motion.

Customise it

Set --ms-accent, --ms-tint, --ms-line and --ms-ring on .ms in the plain CSS and Bootstrap versions; the tick and dash are SVG data URIs in --ms-tick and --ms-dash. In Tailwind edit the --color-ms-* theme colours and the ms-tick and ms-dash utilities. data-names on the wrapper sets how many names are listed before the field switches to a count.