Cascading Select

Selection changes available options in another control.

html

<div class="csc csc--fruit" style="--accent:#A8E6C1;">
  <div class="csc__field">
    <label class="csc__label" for="csc-parent">Category</label>
    <select id="csc-parent" class="csc__select" style="border-radius:8px;">
      <option>Fruit</option>
      <option>Vegetable</option>
      <option>Herb</option>
    </select>
  </div>
  <div class="csc__field csc__field--fruit">
    <label class="csc__label" for="csc-item-fruit">Item</label>
    <select id="csc-item-fruit" class="csc__select" style="border-radius:8px;">
      <option>Apple</option>
      <option>Pear</option>
      <option>Plum</option>
    </select>
  </div>
  <div class="csc__field csc__field--veg">
    <label class="csc__label" for="csc-item-veg">Item</label>
    <select id="csc-item-veg" class="csc__select" style="border-radius:8px;">
      <option>Carrot</option>
      <option>Pea</option>
      <option>Leek</option>
    </select>
  </div>
  <div class="csc__field csc__field--herb">
    <label class="csc__label" for="csc-item-herb">Item</label>
    <select id="csc-item-herb" class="csc__select" style="border-radius:8px;">
      <option>Basil</option>
      <option>Mint</option>
      <option>Thyme</option>
    </select>
  </div>
</div>
<style>
.csc { display: flex; gap: 12px; font: 14px system-ui, sans-serif; color: #241430; max-width: 360px; flex-wrap: wrap; }
.csc__field { flex: 1; min-width: 140px; }
.csc__field--fruit, .csc__field--veg, .csc__field--herb { display: none; }
.csc--fruit .csc__field--fruit, .csc--veg .csc__field--veg, .csc--herb .csc__field--herb { display: block; }
.csc__label { display: block; margin-bottom: 6px; font-weight: 500; }
.csc__select { width: 100%; box-sizing: border-box; padding: 10px 12px; font: inherit; color: #241430; background: #FAF7F2; border: 1.5px solid #24143040; appearance: none; }
.csc__select:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 35%, transparent); }
</style>

tailwind

<div class="csc-tw csc-tw--fruit flex gap-3 flex-wrap font-sans text-sm text-[#241430] max-w-[360px]">
  <div class="flex-1 min-w-[140px]">
    <label class="block mb-1.5 font-medium" for="csc-parent-tw">Category</label>
    <select id="csc-parent-tw" style="border-radius:8px;"
      class="w-full box-border px-3 py-2.5 bg-[#FAF7F2] border-[1.5px] border-[#24143040] appearance-none focus-visible:outline-none">
      <option>Fruit</option>
      <option>Vegetable</option>
      <option>Herb</option>
    </select>
  </div>
  <div class="csc-tw-item csc-tw-item--fruit flex-1 min-w-[140px] hidden">
    <label class="block mb-1.5 font-medium" for="csc-item-fruit-tw">Item</label>
    <select id="csc-item-fruit-tw" style="border-radius:8px;"
      class="w-full box-border px-3 py-2.5 bg-[#FAF7F2] border-[1.5px] border-[#24143040] appearance-none focus-visible:outline-none">
      <option>Apple</option>
      <option>Pear</option>
      <option>Plum</option>
    </select>
  </div>
  <div class="csc-tw-item csc-tw-item--veg flex-1 min-w-[140px] hidden">
    <label class="block mb-1.5 font-medium" for="csc-item-veg-tw">Item</label>
    <select id="csc-item-veg-tw" style="border-radius:8px;"
      class="w-full box-border px-3 py-2.5 bg-[#FAF7F2] border-[1.5px] border-[#24143040] appearance-none focus-visible:outline-none">
      <option>Carrot</option>
      <option>Pea</option>
      <option>Leek</option>
    </select>
  </div>
  <div class="csc-tw-item csc-tw-item--herb flex-1 min-w-[140px] hidden">
    <label class="block mb-1.5 font-medium" for="csc-item-herb-tw">Item</label>
    <select id="csc-item-herb-tw" style="border-radius:8px;"
      class="w-full box-border px-3 py-2.5 bg-[#FAF7F2] border-[1.5px] border-[#24143040] appearance-none focus-visible:outline-none">
      <option>Basil</option>
      <option>Mint</option>
      <option>Thyme</option>
    </select>
  </div>
</div>
<style>
.csc-tw--fruit .csc-tw-item--fruit, .csc-tw--veg .csc-tw-item--veg, .csc-tw--herb .csc-tw-item--herb { display: block !important; }
select:focus-visible { border-color: #A8E6C1 !important; }
</style>

react

export function CascadingSelect({
  label = "Category",
  itemLabel = "Item",
  parent = "fruit",
  accent = "#A8E6C1",
  radius = 8,
}) {
  const itemsByParent = {
    fruit: ["Apple", "Pear", "Plum"],
    veg: ["Carrot", "Pea", "Leek"],
    herb: ["Basil", "Mint", "Thyme"],
  };
  const items = itemsByParent[parent] ?? itemsByParent.fruit;
  const selectStyle = {
    width: "100%", boxSizing: "border-box", padding: "10px 12px", font: "inherit",
    color: "#241430", background: "#FAF7F2", border: "1.5px solid #24143040", borderRadius: radius, appearance: "none",
  };
  return (
    <div style={{ display: "flex", gap: 12, flexWrap: "wrap", fontFamily: "system-ui, sans-serif", fontSize: 14, color: "#241430", maxWidth: 360 }}>
      <div style={{ flex: 1, minWidth: 140 }}>
        <label htmlFor="csc-parent-r" style={{ display: "block", marginBottom: 6, fontWeight: 500 }}>{label}</label>
        <select id="csc-parent-r" style={selectStyle} defaultValue={parent}>
          <option value="fruit">Fruit</option>
          <option value="veg">Vegetable</option>
          <option value="herb">Herb</option>
        </select>
      </div>
      <div style={{ flex: 1, minWidth: 140 }}>
        <label htmlFor="csc-item-r" style={{ display: "block", marginBottom: 6, fontWeight: 500 }}>{itemLabel}</label>
        <select id="csc-item-r" style={{ ...selectStyle, borderColor: accent }}>
          {items.map((item) => <option key={item}>{item}</option>)}
        </select>
      </div>
    </div>
  );
}