Open demo

CSS code

HTML
<div><span class="lbl">Assignee, grouped by team, with avatars</span><div class="card">
<div class="rs" data-select>
  <label class="rs-label" id="rs-as-label" for="rs-as-native">Assignee</label>
  <select class="rs-native" id="rs-as-native" name="assignee">
    <optgroup label="Suggested"><option value="">Unassigned</option><option value="priya">Priya Nair</option></optgroup>
    <optgroup label="Design"><option value="mateo">Mateo Ruiz</option><option value="hana">Hana Kobayashi</option></optgroup>
    <optgroup label="Engineering"><option value="lina" selected>Lina Berg</option><option value="kwame">Kwame Mensah</option><option value="jonas">Jonas Weber</option></optgroup>
  </select>
  <div class="rs-ui" data-ui hidden>
    <div class="rs-combo" id="rs-as-combo" role="combobox" tabindex="0" aria-haspopup="listbox" aria-expanded="false" aria-controls="rs-as-list" aria-labelledby="rs-as-label">
      <span class="rs-shown" data-shown data-placeholder="Unassigned">Unassigned</span>
      <svg class="rs-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="rs-pop" data-popup hidden>
      <div class="rs-list" role="listbox" id="rs-as-list" aria-labelledby="rs-as-label" tabindex="-1">
        <div role="group" aria-labelledby="rs-as-g1">
          <div class="rs-group" role="presentation" id="rs-as-g1">Suggested</div>
          <div class="rs-opt" role="option" id="rs-as-1" data-value="" data-label="Unassigned" aria-selected="false"><span class="rs-av is-none" data-pic aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-dasharray="3 3"><circle cx="12" cy="12" r="9"/></svg></span><span class="rs-main">Unassigned <span class="rs-sub">No owner yet</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
          <div class="rs-opt" role="option" id="rs-as-2" data-value="priya" data-label="Priya Nair" aria-selected="false"><span class="rs-av" style="--tone:#be185d" data-pic aria-hidden="true">PN</span><span class="rs-main">Priya Nair <span class="rs-sub">Product designer · you</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
        </div>
        <div role="group" aria-labelledby="rs-as-g2">
          <div class="rs-group" role="presentation" id="rs-as-g2">Design</div>
          <div class="rs-opt" role="option" id="rs-as-3" data-value="mateo" data-label="Mateo Ruiz" aria-selected="false"><span class="rs-av" style="--tone:#7c3aed" data-pic aria-hidden="true">MR</span><span class="rs-main">Mateo Ruiz <span class="rs-sub">Design lead</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
          <div class="rs-opt" role="option" id="rs-as-4" data-value="hana" data-label="Hana Kobayashi" aria-selected="false"><span class="rs-av" style="--tone:#b45309" data-pic aria-hidden="true">HK</span><span class="rs-main">Hana Kobayashi <span class="rs-sub">Illustrator</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
        </div>
        <div role="group" aria-labelledby="rs-as-g3">
          <div class="rs-group" role="presentation" id="rs-as-g3">Engineering</div>
          <div class="rs-opt" role="option" id="rs-as-5" data-value="lina" data-label="Lina Berg" aria-selected="false"><span class="rs-av" style="--tone:#0369a1" data-pic aria-hidden="true">LB</span><span class="rs-main">Lina Berg <span class="rs-sub">Frontend engineer</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
          <div class="rs-opt" role="option" id="rs-as-6" data-value="kwame" data-label="Kwame Mensah" aria-selected="false"><span class="rs-av" style="--tone:#15803d" data-pic aria-hidden="true">KM</span><span class="rs-main">Kwame Mensah <span class="rs-sub">Backend engineer</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
          <div class="rs-opt" role="option" id="rs-as-7" data-value="jonas" data-label="Jonas Weber" aria-selected="false"><span class="rs-av" style="--tone:#4338ca" data-pic aria-hidden="true">JW</span><span class="rs-main">Jonas Weber <span class="rs-sub">Engineering manager</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
        </div>
      </div>
    </div>
  </div>
</div>
</div></div>

<div><span class="lbl">Project picker, grouped by workspace, with icons</span><div class="card">
<div class="rs" data-select>
  <label class="rs-label" id="rs-pj-label" for="rs-pj-native">Move to project</label>
  <select class="rs-native" id="rs-pj-native" name="project">
    <optgroup label="Northwind Studio"><option value="web">Website redesign</option><option value="brand" selected>Brand refresh</option><option value="q4">Q4 campaign</option></optgroup>
    <optgroup label="Kite Labs"><option value="app">Mobile app 2.0</option><option value="onb">Onboarding flow</option><option value="ds">Design system</option></optgroup>
  </select>
  <div class="rs-ui" data-ui hidden>
    <div class="rs-combo" id="rs-pj-combo" role="combobox" tabindex="0" aria-haspopup="listbox" aria-expanded="false" aria-controls="rs-pj-list" aria-labelledby="rs-pj-label">
      <span class="rs-shown" data-shown data-placeholder="Choose a project">Choose a project</span>
      <svg class="rs-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="rs-pop" data-popup hidden>
      <div class="rs-list" role="listbox" id="rs-pj-list" aria-labelledby="rs-pj-label" tabindex="-1">
        <div role="group" aria-labelledby="rs-pj-g1">
          <div class="rs-group" role="presentation" id="rs-pj-g1">Northwind Studio</div>
          <div class="rs-opt" role="option" id="rs-pj-1" data-value="web" data-label="Website redesign" aria-selected="false"><span class="rs-av is-square" style="--tone:#2563eb" data-pic aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c2.5 2.7 3.7 5.7 3.7 9s-1.2 6.3-3.7 9c-2.5-2.7-3.7-5.7-3.7-9S9.5 5.7 12 3Z"/></svg></span><span class="rs-main">Website redesign <span class="rs-sub">24 open tasks</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
          <div class="rs-opt" role="option" id="rs-pj-2" data-value="brand" data-label="Brand refresh" aria-selected="false"><span class="rs-av is-square" style="--tone:#db2777" data-pic aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 5 4 4L8 20H4v-4L15 5Z"/><path d="m13 7 4 4"/></svg></span><span class="rs-main">Brand refresh <span class="rs-sub">9 open tasks</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
          <div class="rs-opt" role="option" id="rs-pj-3" data-value="q4" data-label="Q4 campaign" aria-selected="false"><span class="rs-av is-square" style="--tone:#c2410c" data-pic aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 10v4h3l6 4V6L6 10H3Z"/><path d="M16 9a4 4 0 0 1 0 6"/></svg></span><span class="rs-main">Q4 campaign <span class="rs-sub">17 open tasks</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
        </div>
        <div role="group" aria-labelledby="rs-pj-g2">
          <div class="rs-group" role="presentation" id="rs-pj-g2">Kite Labs</div>
          <div class="rs-opt" role="option" id="rs-pj-4" data-value="app" data-label="Mobile app 2.0" aria-selected="false"><span class="rs-av is-square" style="--tone:#0f766e" data-pic aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="3" width="10" height="18" rx="2.5"/><path d="M11 17.5h2"/></svg></span><span class="rs-main">Mobile app 2.0 <span class="rs-sub">41 open tasks</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
          <div class="rs-opt" role="option" id="rs-pj-5" data-value="onb" data-label="Onboarding flow" aria-selected="false"><span class="rs-av is-square" style="--tone:#7c3aed" data-pic aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 21V4"/><path d="M5 4h11l-2 4 2 4H5"/></svg></span><span class="rs-main">Onboarding flow <span class="rs-sub">6 open tasks</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
          <div class="rs-opt" role="option" id="rs-pj-6" data-value="ds" data-label="Design system" aria-selected="false"><span class="rs-av is-square" style="--tone:#475569" data-pic aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="7" height="7" rx="1.5"/><rect x="13" y="4" width="7" height="7" rx="1.5"/><rect x="4" y="13" width="7" height="7" rx="1.5"/><rect x="13" y="13" width="7" height="7" rx="1.5"/></svg></span><span class="rs-main">Design system <span class="rs-sub">12 open tasks</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
        </div>
      </div>
    </div>
  </div>
</div>
</div></div>
CSS
/* Grouped select with avatars and icons: a select-only combobox (WAI-ARIA APG) over a listbox split into labelled
   groups. The trigger shows the picked option's avatar or icon, copied from its [data-pic]. The script sets
   aria-expanded, data-active and data-chosen; the hidden <select> (with matching <optgroup>s) keeps the form value. */
.rs{--rs-accent:#c2410c;--rs-tint:#fdf0e7;--rs-ring:rgba(194,65,12,.25);--rs-line:#938a82;--rs-ink:#231a14;--rs-muted:#665b52;
  position:relative;display:grid;grid-template-columns:minmax(0,1fr);gap:7px;color:var(--rs-ink)}
.rs [hidden]{display:none!important}
.rs-label{font-size:.9rem;font-weight:600}
.rs-native{width:100%;height:46px;padding:0 10px;font:inherit;color:inherit;background:#fff;border:1px solid var(--rs-line);border-radius:12px}
.rs-ui{position:relative}
.rs-combo{display:flex;align-items:center;gap:10px;height:50px;padding:0 14px 0 11px;font-size:.95rem;background:#fff;border:1px solid var(--rs-line);
  border-radius:12px;box-shadow:0 1px 2px rgba(35,26,20,.06);cursor:pointer;user-select:none;transition:border-color .15s,box-shadow .15s}
.rs-combo:hover{border-color:#6f665e}
.rs-combo:focus-visible,.rs-combo[aria-expanded="true"]{outline:none;border-color:var(--rs-accent);box-shadow:0 0 0 4px var(--rs-ring)}
.rs-shown{flex:1;min-width:0;display:flex;align-items:center;gap:10px;font-weight:600;white-space:nowrap;overflow:hidden}
.rs-shown .rs-av{width:28px;height:28px;font-size:.68rem}
.rs-chev{flex:none;width:16px;height:16px;color:var(--rs-muted);transition:transform .2s}
.rs-combo[aria-expanded="true"] .rs-chev{transform:rotate(180deg)}
.rs-pop{position:absolute;z-index:30;left:0;right:0;top:calc(100% + 6px);padding:4px 6px 6px;background:#fff;border:1px solid #e0d7ce;border-radius:14px;
  box-shadow:0 18px 40px -12px rgba(35,26,20,.3)}
.rs-list{max-height:320px;overflow-y:auto;overscroll-behavior:contain;outline:none}
.rs-group{padding:10px 10px 5px;font-size:.7rem;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--rs-muted)}
.rs-opt{display:flex;align-items:center;gap:11px;padding:7px 10px;border-radius:9px;cursor:pointer}
.rs-opt[data-active]{background:var(--rs-tint)}
.rs-av{flex:none;display:grid;place-items:center;width:34px;height:34px;border-radius:50%;background:var(--tone);color:#fff;font-size:.74rem;font-weight:700;letter-spacing:.02em}
.rs-av svg{width:58%;height:58%}
.rs-av.is-square{border-radius:9px}
.rs-av.is-none{background:#f3eee9;color:#7c7168}
.rs-main{flex:1;min-width:0;font-size:.93rem;font-weight:600}
.rs-sub{display:block;font-size:.8rem;font-weight:400;color:var(--rs-muted)}
.rs-check{flex:none;width:16px;height:16px;color:var(--rs-accent);visibility:hidden}
.rs-opt[data-chosen] .rs-check{visibility:visible}
@media (prefers-reduced-motion:reduce){.rs-combo,.rs-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">Assignee, grouped by team, with avatars</span><div class="card demo-card"><div class="card-body">
<div class="rs" data-select>
  <label class="form-label" id="rs-as-label" for="rs-as-native">Assignee</label>
  <select class="form-select rs-native" id="rs-as-native" name="assignee">
    <optgroup label="Suggested"><option value="">Unassigned</option><option value="priya">Priya Nair</option></optgroup>
    <optgroup label="Design"><option value="mateo">Mateo Ruiz</option><option value="hana">Hana Kobayashi</option></optgroup>
    <optgroup label="Engineering"><option value="lina" selected>Lina Berg</option><option value="kwame">Kwame Mensah</option><option value="jonas">Jonas Weber</option></optgroup>
  </select>
  <div class="position-relative" data-ui hidden>
    <div class="form-select rs-combo d-flex align-items-center" id="rs-as-combo" role="combobox" tabindex="0" aria-haspopup="listbox" aria-expanded="false" aria-controls="rs-as-list" aria-labelledby="rs-as-label">
      <span class="rs-shown d-flex align-items-center flex-grow-1" data-shown data-placeholder="Unassigned">Unassigned</span>
      <svg class="rs-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 rs-pop" data-popup hidden>
      <div class="rs-list" role="listbox" id="rs-as-list" aria-labelledby="rs-as-label" tabindex="-1">
        <div role="group" aria-labelledby="rs-as-g1">
          <div class="dropdown-header" role="presentation" id="rs-as-g1">Suggested</div>
          <div class="dropdown-item rs-opt d-flex align-items-center" role="option" id="rs-as-1" data-value="" data-label="Unassigned" aria-selected="false"><span class="rs-av is-none" data-pic aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-dasharray="3 3"><circle cx="12" cy="12" r="9"/></svg></span><span class="rs-main">Unassigned <span class="rs-sub">No owner yet</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
          <div class="dropdown-item rs-opt d-flex align-items-center" role="option" id="rs-as-2" data-value="priya" data-label="Priya Nair" aria-selected="false"><span class="rs-av" style="--tone:#be185d" data-pic aria-hidden="true">PN</span><span class="rs-main">Priya Nair <span class="rs-sub">Product designer · you</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
        </div>
        <div role="group" aria-labelledby="rs-as-g2">
          <div class="dropdown-header" role="presentation" id="rs-as-g2">Design</div>
          <div class="dropdown-item rs-opt d-flex align-items-center" role="option" id="rs-as-3" data-value="mateo" data-label="Mateo Ruiz" aria-selected="false"><span class="rs-av" style="--tone:#7c3aed" data-pic aria-hidden="true">MR</span><span class="rs-main">Mateo Ruiz <span class="rs-sub">Design lead</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
          <div class="dropdown-item rs-opt d-flex align-items-center" role="option" id="rs-as-4" data-value="hana" data-label="Hana Kobayashi" aria-selected="false"><span class="rs-av" style="--tone:#b45309" data-pic aria-hidden="true">HK</span><span class="rs-main">Hana Kobayashi <span class="rs-sub">Illustrator</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
        </div>
        <div role="group" aria-labelledby="rs-as-g3">
          <div class="dropdown-header" role="presentation" id="rs-as-g3">Engineering</div>
          <div class="dropdown-item rs-opt d-flex align-items-center" role="option" id="rs-as-5" data-value="lina" data-label="Lina Berg" aria-selected="false"><span class="rs-av" style="--tone:#0369a1" data-pic aria-hidden="true">LB</span><span class="rs-main">Lina Berg <span class="rs-sub">Frontend engineer</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
          <div class="dropdown-item rs-opt d-flex align-items-center" role="option" id="rs-as-6" data-value="kwame" data-label="Kwame Mensah" aria-selected="false"><span class="rs-av" style="--tone:#15803d" data-pic aria-hidden="true">KM</span><span class="rs-main">Kwame Mensah <span class="rs-sub">Backend engineer</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
          <div class="dropdown-item rs-opt d-flex align-items-center" role="option" id="rs-as-7" data-value="jonas" data-label="Jonas Weber" aria-selected="false"><span class="rs-av" style="--tone:#4338ca" data-pic aria-hidden="true">JW</span><span class="rs-main">Jonas Weber <span class="rs-sub">Engineering manager</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
        </div>
      </div>
    </div>
  </div>
</div>
</div></div></div>

<div><span class="lbl">Project picker, grouped by workspace, with icons</span><div class="card demo-card"><div class="card-body">
<div class="rs" data-select>
  <label class="form-label" id="rs-pj-label" for="rs-pj-native">Move to project</label>
  <select class="form-select rs-native" id="rs-pj-native" name="project">
    <optgroup label="Northwind Studio"><option value="web">Website redesign</option><option value="brand" selected>Brand refresh</option><option value="q4">Q4 campaign</option></optgroup>
    <optgroup label="Kite Labs"><option value="app">Mobile app 2.0</option><option value="onb">Onboarding flow</option><option value="ds">Design system</option></optgroup>
  </select>
  <div class="position-relative" data-ui hidden>
    <div class="form-select rs-combo d-flex align-items-center" id="rs-pj-combo" role="combobox" tabindex="0" aria-haspopup="listbox" aria-expanded="false" aria-controls="rs-pj-list" aria-labelledby="rs-pj-label">
      <span class="rs-shown d-flex align-items-center flex-grow-1" data-shown data-placeholder="Choose a project">Choose a project</span>
      <svg class="rs-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 rs-pop" data-popup hidden>
      <div class="rs-list" role="listbox" id="rs-pj-list" aria-labelledby="rs-pj-label" tabindex="-1">
        <div role="group" aria-labelledby="rs-pj-g1">
          <div class="dropdown-header" role="presentation" id="rs-pj-g1">Northwind Studio</div>
          <div class="dropdown-item rs-opt d-flex align-items-center" role="option" id="rs-pj-1" data-value="web" data-label="Website redesign" aria-selected="false"><span class="rs-av is-square" style="--tone:#2563eb" data-pic aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c2.5 2.7 3.7 5.7 3.7 9s-1.2 6.3-3.7 9c-2.5-2.7-3.7-5.7-3.7-9S9.5 5.7 12 3Z"/></svg></span><span class="rs-main">Website redesign <span class="rs-sub">24 open tasks</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
          <div class="dropdown-item rs-opt d-flex align-items-center" role="option" id="rs-pj-2" data-value="brand" data-label="Brand refresh" aria-selected="false"><span class="rs-av is-square" style="--tone:#db2777" data-pic aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 5 4 4L8 20H4v-4L15 5Z"/><path d="m13 7 4 4"/></svg></span><span class="rs-main">Brand refresh <span class="rs-sub">9 open tasks</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
          <div class="dropdown-item rs-opt d-flex align-items-center" role="option" id="rs-pj-3" data-value="q4" data-label="Q4 campaign" aria-selected="false"><span class="rs-av is-square" style="--tone:#c2410c" data-pic aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 10v4h3l6 4V6L6 10H3Z"/><path d="M16 9a4 4 0 0 1 0 6"/></svg></span><span class="rs-main">Q4 campaign <span class="rs-sub">17 open tasks</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
        </div>
        <div role="group" aria-labelledby="rs-pj-g2">
          <div class="dropdown-header" role="presentation" id="rs-pj-g2">Kite Labs</div>
          <div class="dropdown-item rs-opt d-flex align-items-center" role="option" id="rs-pj-4" data-value="app" data-label="Mobile app 2.0" aria-selected="false"><span class="rs-av is-square" style="--tone:#0f766e" data-pic aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="3" width="10" height="18" rx="2.5"/><path d="M11 17.5h2"/></svg></span><span class="rs-main">Mobile app 2.0 <span class="rs-sub">41 open tasks</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
          <div class="dropdown-item rs-opt d-flex align-items-center" role="option" id="rs-pj-5" data-value="onb" data-label="Onboarding flow" aria-selected="false"><span class="rs-av is-square" style="--tone:#7c3aed" data-pic aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 21V4"/><path d="M5 4h11l-2 4 2 4H5"/></svg></span><span class="rs-main">Onboarding flow <span class="rs-sub">6 open tasks</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
          <div class="dropdown-item rs-opt d-flex align-items-center" role="option" id="rs-pj-6" data-value="ds" data-label="Design system" aria-selected="false"><span class="rs-av is-square" style="--tone:#475569" data-pic aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="7" height="7" rx="1.5"/><rect x="13" y="4" width="7" height="7" rx="1.5"/><rect x="4" y="13" width="7" height="7" rx="1.5"/><rect x="13" y="13" width="7" height="7" rx="1.5"/></svg></span><span class="rs-main">Design system <span class="rs-sub">12 open tasks</span></span><svg class="rs-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
        </div>
      </div>
    </div>
  </div>
</div>
</div></div></div>
CSS
/* Grouped select with avatars and icons on Bootstrap parts: a .form-select look for the select-only combobox
   (WAI-ARIA APG), a .dropdown-menu with .dropdown-header group labels and .dropdown-item options. The trigger shows the
   picked option's [data-pic]. The script sets aria-expanded, data-active and data-chosen; a hidden <select> keeps the value. */
.rs{--rs-accent:#c2410c;--rs-tint:#fdf0e7;--rs-ring:rgba(194,65,12,.25);--rs-line:#938a82;--rs-ink:#231a14;--rs-muted:#665b52;
  position:relative;display:grid;grid-template-columns:minmax(0,1fr);gap:7px;color:var(--rs-ink)}
.rs [hidden]{display:none!important} /* beats .d-flex */
.rs .form-label{margin:0;font-size:.9rem;font-weight:600}
.rs-native{border-color:var(--rs-line);border-radius:12px;height:46px}
.rs .rs-combo{--bs-form-select-bg-img:none;gap:10px;height:50px;padding:0 14px 0 11px;font-size:.95rem;line-height:1.45;color:var(--rs-ink);border-color:var(--rs-line);
  border-radius:12px;box-shadow:0 1px 2px rgba(35,26,20,.06);cursor:pointer;user-select:none}
.rs .rs-combo:hover{border-color:#6f665e}
.rs .rs-combo:focus{border-color:var(--rs-line);box-shadow:0 1px 2px rgba(35,26,20,.06)}
.rs .rs-combo:focus-visible,.rs .rs-combo[aria-expanded="true"]{border-color:var(--rs-accent);box-shadow:0 0 0 4px var(--rs-ring)}
.rs-shown{gap:10px;font-weight:600;white-space:nowrap;overflow:hidden}
.rs-shown .rs-av{width:28px;height:28px;font-size:.68rem}
.rs-chev{flex:none;width:16px;height:16px;color:var(--rs-muted);transition:transform .2s}
.rs-combo[aria-expanded="true"] .rs-chev{transform:rotate(180deg)}
.rs .rs-pop{--bs-dropdown-padding-x:6px;--bs-dropdown-padding-y:4px;--bs-dropdown-border-color:#e0d7ce;--bs-dropdown-border-radius:14px;
  --bs-dropdown-link-color:var(--rs-ink);--bs-dropdown-link-hover-color:var(--rs-ink);--bs-dropdown-link-hover-bg:transparent;
  --bs-dropdown-link-active-color:var(--rs-ink);--bs-dropdown-link-active-bg:transparent;--bs-dropdown-header-color:var(--rs-muted);
  --bs-dropdown-header-padding-x:10px;--bs-dropdown-header-padding-y:10px;
  display:block;left:0;right:0;top:calc(100% + 6px);z-index:30;padding-bottom:6px;box-shadow:0 18px 40px -12px rgba(35,26,20,.3)}
.rs-list{max-height:320px;overflow-y:auto;overscroll-behavior:contain;outline:none}
.rs .dropdown-header{padding-bottom:5px;font-size:.7rem;font-weight:700;letter-spacing:.09em;text-transform:uppercase}
.rs .rs-opt{gap:11px;padding:7px 10px;border-radius:9px;white-space:normal;cursor:pointer}
.rs .rs-opt[data-active]{background:var(--rs-tint)}
.rs-av{flex:none;display:grid;place-items:center;width:34px;height:34px;border-radius:50%;background:var(--tone);color:#fff;font-size:.74rem;font-weight:700;letter-spacing:.02em}
.rs-av svg{width:58%;height:58%}
.rs-av.is-square{border-radius:9px}
.rs-av.is-none{background:#f3eee9;color:#7c7168}
.rs-main{flex:1;min-width:0;font-size:.93rem;font-weight:600}
.rs-sub{display:block;font-size:.8rem;font-weight:400;color:var(--rs-muted)}
.rs-check{flex:none;width:16px;height:16px;color:var(--rs-accent);visibility:hidden}
.rs-opt[data-chosen] .rs-check{visibility:visible}
@media (prefers-reduced-motion:reduce){.rs .rs-combo,.rs-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-[#665b52] uppercase">Assignee, grouped by team, with avatars</span><div class="rounded-2xl border border-[#ece4dc] bg-white px-6 pt-[22px] pb-6 shadow-[0_1px_2px_rgba(35,26,20,.04)]">
<div class="relative grid grid-cols-[minmax(0,1fr)] gap-[7px] text-rs-ink" data-select>
  <label class="text-[.9rem] font-semibold" id="rs-as-label" for="rs-as-native">Assignee</label>
  <select class="h-[46px] w-full rounded-xl border border-rs-line bg-white px-2.5" id="rs-as-native" name="assignee">
    <optgroup label="Suggested"><option value="">Unassigned</option><option value="priya">Priya Nair</option></optgroup>
    <optgroup label="Design"><option value="mateo">Mateo Ruiz</option><option value="hana">Hana Kobayashi</option></optgroup>
    <optgroup label="Engineering"><option value="lina" selected>Lina Berg</option><option value="kwame">Kwame Mensah</option><option value="jonas">Jonas Weber</option></optgroup>
  </select>
  <div class="relative" data-ui hidden>
    <div class="group flex h-[50px] cursor-pointer items-center gap-2.5 rounded-xl border border-rs-line bg-white pr-3.5 pl-[11px] text-[.95rem] shadow-[0_1px_2px_rgba(35,26,20,.06)] select-none transition-[border-color,box-shadow] duration-150 hover:border-[#6f665e] focus-visible:border-rs-accent focus-visible:shadow-[0_0_0_4px_rgba(194,65,12,.25)] focus-visible:outline-none aria-expanded:border-rs-accent aria-expanded:shadow-[0_0_0_4px_rgba(194,65,12,.25)] motion-reduce:transition-none" id="rs-as-combo" role="combobox" tabindex="0" aria-haspopup="listbox" aria-expanded="false" aria-controls="rs-as-list" aria-labelledby="rs-as-label">
      <span class="flex min-w-0 flex-1 items-center gap-2.5 overflow-hidden font-semibold whitespace-nowrap [&_[data-pic]]:size-7 [&_[data-pic]]:text-[.68rem]" data-shown data-placeholder="Unassigned">Unassigned</span>
      <svg class="size-4 flex-none text-rs-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-[14px] border border-[#e0d7ce] bg-white px-1.5 pt-1 pb-1.5 shadow-[0_18px_40px_-12px_rgba(35,26,20,.3)]" data-popup hidden>
      <div class="max-h-[320px] overflow-y-auto overscroll-contain outline-none" role="listbox" id="rs-as-list" aria-labelledby="rs-as-label" tabindex="-1">
        <div role="group" aria-labelledby="rs-as-g1">
          <div class="px-2.5 pt-2.5 pb-[5px] text-[.7rem] font-bold tracking-[.09em] text-rs-muted uppercase" role="presentation" id="rs-as-g1">Suggested</div>
          <div class="group flex cursor-pointer items-center gap-[11px] rounded-[9px] px-2.5 py-[7px] data-active:bg-rs-tint" role="option" id="rs-as-1" data-value="" data-label="Unassigned" aria-selected="false"><span class="grid size-[34px] flex-none place-items-center rounded-full bg-(--tone) text-[.74rem] font-bold tracking-[.02em] text-white [&_svg]:size-[58%] !bg-[#f3eee9] !text-[#7c7168]" data-pic aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-dasharray="3 3"><circle cx="12" cy="12" r="9"/></svg></span><span class="min-w-0 flex-1 text-[.93rem] font-semibold">Unassigned <span class="block text-[.8rem] font-normal text-rs-muted">No owner yet</span></span><svg class="invisible size-4 flex-none text-rs-accent group-data-chosen:visible" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
          <div class="group flex cursor-pointer items-center gap-[11px] rounded-[9px] px-2.5 py-[7px] data-active:bg-rs-tint" role="option" id="rs-as-2" data-value="priya" data-label="Priya Nair" aria-selected="false"><span class="grid size-[34px] flex-none place-items-center rounded-full bg-(--tone) text-[.74rem] font-bold tracking-[.02em] text-white [&_svg]:size-[58%]" style="--tone:#be185d" data-pic aria-hidden="true">PN</span><span class="min-w-0 flex-1 text-[.93rem] font-semibold">Priya Nair <span class="block text-[.8rem] font-normal text-rs-muted">Product designer · you</span></span><svg class="invisible size-4 flex-none text-rs-accent group-data-chosen:visible" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
        </div>
        <div role="group" aria-labelledby="rs-as-g2">
          <div class="px-2.5 pt-2.5 pb-[5px] text-[.7rem] font-bold tracking-[.09em] text-rs-muted uppercase" role="presentation" id="rs-as-g2">Design</div>
          <div class="group flex cursor-pointer items-center gap-[11px] rounded-[9px] px-2.5 py-[7px] data-active:bg-rs-tint" role="option" id="rs-as-3" data-value="mateo" data-label="Mateo Ruiz" aria-selected="false"><span class="grid size-[34px] flex-none place-items-center rounded-full bg-(--tone) text-[.74rem] font-bold tracking-[.02em] text-white [&_svg]:size-[58%]" style="--tone:#7c3aed" data-pic aria-hidden="true">MR</span><span class="min-w-0 flex-1 text-[.93rem] font-semibold">Mateo Ruiz <span class="block text-[.8rem] font-normal text-rs-muted">Design lead</span></span><svg class="invisible size-4 flex-none text-rs-accent group-data-chosen:visible" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
          <div class="group flex cursor-pointer items-center gap-[11px] rounded-[9px] px-2.5 py-[7px] data-active:bg-rs-tint" role="option" id="rs-as-4" data-value="hana" data-label="Hana Kobayashi" aria-selected="false"><span class="grid size-[34px] flex-none place-items-center rounded-full bg-(--tone) text-[.74rem] font-bold tracking-[.02em] text-white [&_svg]:size-[58%]" style="--tone:#b45309" data-pic aria-hidden="true">HK</span><span class="min-w-0 flex-1 text-[.93rem] font-semibold">Hana Kobayashi <span class="block text-[.8rem] font-normal text-rs-muted">Illustrator</span></span><svg class="invisible size-4 flex-none text-rs-accent group-data-chosen:visible" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
        </div>
        <div role="group" aria-labelledby="rs-as-g3">
          <div class="px-2.5 pt-2.5 pb-[5px] text-[.7rem] font-bold tracking-[.09em] text-rs-muted uppercase" role="presentation" id="rs-as-g3">Engineering</div>
          <div class="group flex cursor-pointer items-center gap-[11px] rounded-[9px] px-2.5 py-[7px] data-active:bg-rs-tint" role="option" id="rs-as-5" data-value="lina" data-label="Lina Berg" aria-selected="false"><span class="grid size-[34px] flex-none place-items-center rounded-full bg-(--tone) text-[.74rem] font-bold tracking-[.02em] text-white [&_svg]:size-[58%]" style="--tone:#0369a1" data-pic aria-hidden="true">LB</span><span class="min-w-0 flex-1 text-[.93rem] font-semibold">Lina Berg <span class="block text-[.8rem] font-normal text-rs-muted">Frontend engineer</span></span><svg class="invisible size-4 flex-none text-rs-accent group-data-chosen:visible" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
          <div class="group flex cursor-pointer items-center gap-[11px] rounded-[9px] px-2.5 py-[7px] data-active:bg-rs-tint" role="option" id="rs-as-6" data-value="kwame" data-label="Kwame Mensah" aria-selected="false"><span class="grid size-[34px] flex-none place-items-center rounded-full bg-(--tone) text-[.74rem] font-bold tracking-[.02em] text-white [&_svg]:size-[58%]" style="--tone:#15803d" data-pic aria-hidden="true">KM</span><span class="min-w-0 flex-1 text-[.93rem] font-semibold">Kwame Mensah <span class="block text-[.8rem] font-normal text-rs-muted">Backend engineer</span></span><svg class="invisible size-4 flex-none text-rs-accent group-data-chosen:visible" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
          <div class="group flex cursor-pointer items-center gap-[11px] rounded-[9px] px-2.5 py-[7px] data-active:bg-rs-tint" role="option" id="rs-as-7" data-value="jonas" data-label="Jonas Weber" aria-selected="false"><span class="grid size-[34px] flex-none place-items-center rounded-full bg-(--tone) text-[.74rem] font-bold tracking-[.02em] text-white [&_svg]:size-[58%]" style="--tone:#4338ca" data-pic aria-hidden="true">JW</span><span class="min-w-0 flex-1 text-[.93rem] font-semibold">Jonas Weber <span class="block text-[.8rem] font-normal text-rs-muted">Engineering manager</span></span><svg class="invisible size-4 flex-none text-rs-accent group-data-chosen:visible" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
        </div>
      </div>
    </div>
  </div>
</div>
</div></div>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#665b52] uppercase">Project picker, grouped by workspace, with icons</span><div class="rounded-2xl border border-[#ece4dc] bg-white px-6 pt-[22px] pb-6 shadow-[0_1px_2px_rgba(35,26,20,.04)]">
<div class="relative grid grid-cols-[minmax(0,1fr)] gap-[7px] text-rs-ink" data-select>
  <label class="text-[.9rem] font-semibold" id="rs-pj-label" for="rs-pj-native">Move to project</label>
  <select class="h-[46px] w-full rounded-xl border border-rs-line bg-white px-2.5" id="rs-pj-native" name="project">
    <optgroup label="Northwind Studio"><option value="web">Website redesign</option><option value="brand" selected>Brand refresh</option><option value="q4">Q4 campaign</option></optgroup>
    <optgroup label="Kite Labs"><option value="app">Mobile app 2.0</option><option value="onb">Onboarding flow</option><option value="ds">Design system</option></optgroup>
  </select>
  <div class="relative" data-ui hidden>
    <div class="group flex h-[50px] cursor-pointer items-center gap-2.5 rounded-xl border border-rs-line bg-white pr-3.5 pl-[11px] text-[.95rem] shadow-[0_1px_2px_rgba(35,26,20,.06)] select-none transition-[border-color,box-shadow] duration-150 hover:border-[#6f665e] focus-visible:border-rs-accent focus-visible:shadow-[0_0_0_4px_rgba(194,65,12,.25)] focus-visible:outline-none aria-expanded:border-rs-accent aria-expanded:shadow-[0_0_0_4px_rgba(194,65,12,.25)] motion-reduce:transition-none" id="rs-pj-combo" role="combobox" tabindex="0" aria-haspopup="listbox" aria-expanded="false" aria-controls="rs-pj-list" aria-labelledby="rs-pj-label">
      <span class="flex min-w-0 flex-1 items-center gap-2.5 overflow-hidden font-semibold whitespace-nowrap [&_[data-pic]]:size-7 [&_[data-pic]]:text-[.68rem]" data-shown data-placeholder="Choose a project">Choose a project</span>
      <svg class="size-4 flex-none text-rs-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-[14px] border border-[#e0d7ce] bg-white px-1.5 pt-1 pb-1.5 shadow-[0_18px_40px_-12px_rgba(35,26,20,.3)]" data-popup hidden>
      <div class="max-h-[320px] overflow-y-auto overscroll-contain outline-none" role="listbox" id="rs-pj-list" aria-labelledby="rs-pj-label" tabindex="-1">
        <div role="group" aria-labelledby="rs-pj-g1">
          <div class="px-2.5 pt-2.5 pb-[5px] text-[.7rem] font-bold tracking-[.09em] text-rs-muted uppercase" role="presentation" id="rs-pj-g1">Northwind Studio</div>
          <div class="group flex cursor-pointer items-center gap-[11px] rounded-[9px] px-2.5 py-[7px] data-active:bg-rs-tint" role="option" id="rs-pj-1" data-value="web" data-label="Website redesign" aria-selected="false"><span class="grid size-[34px] flex-none place-items-center rounded-full bg-(--tone) text-[.74rem] font-bold tracking-[.02em] text-white [&_svg]:size-[58%] !rounded-[9px]" style="--tone:#2563eb" data-pic aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c2.5 2.7 3.7 5.7 3.7 9s-1.2 6.3-3.7 9c-2.5-2.7-3.7-5.7-3.7-9S9.5 5.7 12 3Z"/></svg></span><span class="min-w-0 flex-1 text-[.93rem] font-semibold">Website redesign <span class="block text-[.8rem] font-normal text-rs-muted">24 open tasks</span></span><svg class="invisible size-4 flex-none text-rs-accent group-data-chosen:visible" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
          <div class="group flex cursor-pointer items-center gap-[11px] rounded-[9px] px-2.5 py-[7px] data-active:bg-rs-tint" role="option" id="rs-pj-2" data-value="brand" data-label="Brand refresh" aria-selected="false"><span class="grid size-[34px] flex-none place-items-center rounded-full bg-(--tone) text-[.74rem] font-bold tracking-[.02em] text-white [&_svg]:size-[58%] !rounded-[9px]" style="--tone:#db2777" data-pic aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 5 4 4L8 20H4v-4L15 5Z"/><path d="m13 7 4 4"/></svg></span><span class="min-w-0 flex-1 text-[.93rem] font-semibold">Brand refresh <span class="block text-[.8rem] font-normal text-rs-muted">9 open tasks</span></span><svg class="invisible size-4 flex-none text-rs-accent group-data-chosen:visible" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
          <div class="group flex cursor-pointer items-center gap-[11px] rounded-[9px] px-2.5 py-[7px] data-active:bg-rs-tint" role="option" id="rs-pj-3" data-value="q4" data-label="Q4 campaign" aria-selected="false"><span class="grid size-[34px] flex-none place-items-center rounded-full bg-(--tone) text-[.74rem] font-bold tracking-[.02em] text-white [&_svg]:size-[58%] !rounded-[9px]" style="--tone:#c2410c" data-pic aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 10v4h3l6 4V6L6 10H3Z"/><path d="M16 9a4 4 0 0 1 0 6"/></svg></span><span class="min-w-0 flex-1 text-[.93rem] font-semibold">Q4 campaign <span class="block text-[.8rem] font-normal text-rs-muted">17 open tasks</span></span><svg class="invisible size-4 flex-none text-rs-accent group-data-chosen:visible" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
        </div>
        <div role="group" aria-labelledby="rs-pj-g2">
          <div class="px-2.5 pt-2.5 pb-[5px] text-[.7rem] font-bold tracking-[.09em] text-rs-muted uppercase" role="presentation" id="rs-pj-g2">Kite Labs</div>
          <div class="group flex cursor-pointer items-center gap-[11px] rounded-[9px] px-2.5 py-[7px] data-active:bg-rs-tint" role="option" id="rs-pj-4" data-value="app" data-label="Mobile app 2.0" aria-selected="false"><span class="grid size-[34px] flex-none place-items-center rounded-full bg-(--tone) text-[.74rem] font-bold tracking-[.02em] text-white [&_svg]:size-[58%] !rounded-[9px]" style="--tone:#0f766e" data-pic aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="3" width="10" height="18" rx="2.5"/><path d="M11 17.5h2"/></svg></span><span class="min-w-0 flex-1 text-[.93rem] font-semibold">Mobile app 2.0 <span class="block text-[.8rem] font-normal text-rs-muted">41 open tasks</span></span><svg class="invisible size-4 flex-none text-rs-accent group-data-chosen:visible" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
          <div class="group flex cursor-pointer items-center gap-[11px] rounded-[9px] px-2.5 py-[7px] data-active:bg-rs-tint" role="option" id="rs-pj-5" data-value="onb" data-label="Onboarding flow" aria-selected="false"><span class="grid size-[34px] flex-none place-items-center rounded-full bg-(--tone) text-[.74rem] font-bold tracking-[.02em] text-white [&_svg]:size-[58%] !rounded-[9px]" style="--tone:#7c3aed" data-pic aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 21V4"/><path d="M5 4h11l-2 4 2 4H5"/></svg></span><span class="min-w-0 flex-1 text-[.93rem] font-semibold">Onboarding flow <span class="block text-[.8rem] font-normal text-rs-muted">6 open tasks</span></span><svg class="invisible size-4 flex-none text-rs-accent group-data-chosen:visible" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
          <div class="group flex cursor-pointer items-center gap-[11px] rounded-[9px] px-2.5 py-[7px] data-active:bg-rs-tint" role="option" id="rs-pj-6" data-value="ds" data-label="Design system" aria-selected="false"><span class="grid size-[34px] flex-none place-items-center rounded-full bg-(--tone) text-[.74rem] font-bold tracking-[.02em] text-white [&_svg]:size-[58%] !rounded-[9px]" style="--tone:#475569" data-pic aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="7" height="7" rx="1.5"/><rect x="13" y="4" width="7" height="7" rx="1.5"/><rect x="4" y="13" width="7" height="7" rx="1.5"/><rect x="13" y="13" width="7" height="7" rx="1.5"/></svg></span><span class="min-w-0 flex-1 text-[.93rem] font-semibold">Design system <span class="block text-[.8rem] font-normal text-rs-muted">12 open tasks</span></span><svg class="invisible size-4 flex-none text-rs-accent group-data-chosen:visible" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m3.5 8.5 3 3 6-7"/></svg></div>
        </div>
      </div>
    </div>
  </div>
</div>
</div></div>
CSS
@theme {
  --color-rs-accent: #c2410c;
  --color-rs-tint: #fdf0e7;
  --color-rs-line: #938a82;
  --color-rs-ink: #231a14;
  --color-rs-muted: #665b52;
}
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

Single selects whose options need more than a line of text. Assignee, grouped by team, with avatars sorts people into Suggested, Design and Engineering, gives each a coloured initials avatar and a role, and offers Unassigned with a dashed placeholder. Project picker, grouped by workspace, with icons groups projects under two workspaces with a coloured icon and an open task count. The closed field shows the picked option’s avatar or icon next to its name, copied from the option. Each list is a WAI-ARIA listbox with labelled groups, opened by a select-only combobox: Down, Up, Home and End move, Enter or Space picks, typing a letter jumps to the next matching name, Tab picks the highlighted option and moves on, and Escape closes without changing anything. A hidden select with matching optgroups keeps the value, so the form submits the id, and without JavaScript that native grouped select is shown instead. Use it for assignees, owners, projects, folders or any choice people recognise by picture first.

What’s included

  • Labelled option groups inside the listbox
  • Avatars and icons copied into the closed field
  • Secondary line for role or task count
  • Type-ahead that jumps to the next match
  • Hidden select with matching optgroups for form submission

Accessibility

The field is a role="combobox" labelled by the visible label; the listbox contains role="group" sections labelled by their headings, and the highlighted option carries aria-selected as the select-only pattern specifies. Avatars and icons are aria-hidden, so each option is read as its name and role. Focus shows a ring and the chevron turn stops under prefers-reduced-motion.

Customise it

Change --rs-accent, --rs-tint and --rs-line on .rs; each avatar or icon takes its colour from --tone in its style attribute. In Tailwind edit the --color-rs-* theme colours.