Combobox

Searchable/selectable input.

  • Priya Nair
  • Declan Fox
  • Yuki Tanaka

html

<div class="cb" style="--accent:#D9C8F0;">
  <label class="cb__label" for="cb-trigger">Assignee</label>
  <div class="cb__combo cb__combo--priya cb__combo--open" role="combobox" aria-haspopup="listbox" aria-owns="cb-listbox" aria-expanded="true">
    <button type="button" id="cb-trigger" class="cb__trigger" aria-controls="cb-listbox" style="border-radius:10px;">
      <span class="cb__value cb__value--priya">Priya Nair</span>
      <span class="cb__value cb__value--declan">Declan Fox</span>
      <span class="cb__value cb__value--yuki">Yuki Tanaka</span>
      <svg class="cb__chev" viewBox="0 0 20 20" width="14" height="14" aria-hidden="true"><path d="M5 8l5 5 5-5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path></svg>
    </button>
    <ul id="cb-listbox" class="cb__listbox" role="listbox" aria-label="Assignee options">
      <li class="cb__option" data-person="priya" role="option" aria-selected="true">Priya Nair</li>
      <li class="cb__option" data-person="declan" role="option" aria-selected="false">Declan Fox</li>
      <li class="cb__option" data-person="yuki" role="option" aria-selected="false">Yuki Tanaka</li>
    </ul>
  </div>
</div>
<style>
.cb { display: block; font: 14px system-ui, sans-serif; color: #241430; max-width: 260px; }
.cb__label { display: block; margin-bottom: 6px; font-weight: 500; }
.cb__combo { position: relative; }
.cb__trigger { width: 100%; box-sizing: border-box; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 10px 12px; font: inherit; color: #241430; background: #FAF7F2; border: 1.5px solid #24143040; cursor: pointer; }
.cb__trigger:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 35%, transparent); }
.cb__value { display: none; }
.cb__combo--priya .cb__value--priya, .cb__combo--declan .cb__value--declan, .cb__combo--yuki .cb__value--yuki { display: inline; }
.cb__listbox { list-style: none; margin: 4px 0 0; padding: 4px; border: 1.5px solid #24143020; background: #FAF7F2; box-shadow: 0 8px 20px #24143022; display: none; }
.cb__combo--open .cb__listbox { display: block; }
.cb__option { padding: 8px 10px; border-radius: 6px; cursor: pointer; }
.cb__option:hover { background: #24143014; }
.cb__combo--priya .cb__option[data-person="priya"], .cb__combo--declan .cb__option[data-person="declan"], .cb__combo--yuki .cb__option[data-person="yuki"] { background: var(--accent); }
</style>

tailwind

<div class="max-w-[260px] font-sans text-sm text-[#241430]">
  <label class="block mb-1.5 font-medium" for="cb-trigger-tw">Assignee</label>
  <div class="cb-tw cb-tw--priya relative" role="combobox" aria-haspopup="listbox" aria-owns="cb-listbox-tw" aria-expanded="true">
    <button type="button" id="cb-trigger-tw" aria-controls="cb-listbox-tw" style="border-radius:10px;"
      class="w-full box-border flex items-center justify-between gap-2 px-3 py-2.5 bg-[#FAF7F2] border-[1.5px] border-[#24143040] cursor-pointer focus-visible:outline-none">
      <span class="cb-tw-value cb-tw-value--priya hidden">Priya Nair</span>
      <span class="cb-tw-value cb-tw-value--declan hidden">Declan Fox</span>
      <span class="cb-tw-value cb-tw-value--yuki hidden">Yuki Tanaka</span>
      <svg viewBox="0 0 20 20" width="14" height="14" aria-hidden="true"><path d="M5 8l5 5 5-5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path></svg>
    </button>
    <ul id="cb-listbox-tw" role="listbox" aria-label="Assignee options"
      class="block list-none m-0 mt-1 p-1 border-[1.5px] border-[#24143020] bg-[#FAF7F2] shadow-lg">
      <li class="cb-tw-opt px-2.5 py-2 rounded-md cursor-pointer hover:bg-[#24143014]" data-person="priya" role="option" aria-selected="true">Priya Nair</li>
      <li class="cb-tw-opt px-2.5 py-2 rounded-md cursor-pointer hover:bg-[#24143014]" data-person="declan" role="option" aria-selected="false">Declan Fox</li>
      <li class="cb-tw-opt px-2.5 py-2 rounded-md cursor-pointer hover:bg-[#24143014]" data-person="yuki" role="option" aria-selected="false">Yuki Tanaka</li>
    </ul>
  </div>
</div>
<style>
.cb-tw--priya .cb-tw-value--priya, .cb-tw--declan .cb-tw-value--declan, .cb-tw--yuki .cb-tw-value--yuki { display: inline; }
.cb-tw--priya .cb-tw-opt[data-person="priya"], .cb-tw--declan .cb-tw-opt[data-person="declan"], .cb-tw--yuki .cb-tw-opt[data-person="yuki"] { background: #D9C8F0; }
</style>

react

export function Combobox({
  label = "Assignee",
  open = true,
  chosen = "priya",
  accent = "#D9C8F0",
  radius = 10,
}) {
  const people = [
    { id: "priya", name: "Priya Nair" },
    { id: "declan", name: "Declan Fox" },
    { id: "yuki", name: "Yuki Tanaka" },
  ];
  const current = people.find((p) => p.id === chosen) ?? people[0];
  return (
    <div style={{ display: "block", fontFamily: "system-ui, sans-serif", fontSize: 14, color: "#241430", maxWidth: 260 }}>
      <label htmlFor="cb-trigger-r" style={{ display: "block", marginBottom: 6, fontWeight: 500 }}>{label}</label>
      <div style={{ position: "relative" }} role="combobox" aria-haspopup="listbox" aria-owns="cb-listbox-r" aria-expanded={open}>
        <button type="button" id="cb-trigger-r" aria-controls="cb-listbox-r" style={{
          width: "100%", boxSizing: "border-box", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8,
          padding: "10px 12px", font: "inherit", color: "#241430", background: "#FAF7F2", border: "1.5px solid #24143040", borderRadius: radius, cursor: "pointer",
        }}>
          <span>{current.name}</span>
          <svg viewBox="0 0 20 20" width="14" height="14" aria-hidden="true"><path d="M5 8l5 5 5-5" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" /></svg>
        </button>
        <ul id="cb-listbox-r" role="listbox" aria-label={label + " options"} style={{
          display: open ? "block" : "none", listStyle: "none", margin: "4px 0 0", padding: 4,
          border: "1.5px solid #24143020", background: "#FAF7F2", boxShadow: "0 8px 20px #24143022",
        }}>
          {people.map((p) => (
            <li key={p.id} role="option" aria-selected={p.id === chosen} style={{
              padding: "8px 10px", borderRadius: 6, cursor: "pointer", background: p.id === chosen ? accent : "transparent",
            }}>{p.name}</li>
          ))}
        </ul>
      </div>
    </div>
  );
}