Tree Select

Selects from hierarchical data.

Office location

html

<div class="ts" style="--accent:#D9C8F0;border-radius:8px;">
  <div class="ts__label" id="ts-label">Office location</div>
  <ul class="ts__tree ts__tree--london" role="tree" aria-labelledby="ts-label">
    <li class="ts__branch" role="treeitem" aria-expanded="true">
      <span class="ts__branch-label">United Kingdom</span>
      <ul class="ts__group" role="group">
        <li id="ts-london" class="ts__leaf" role="treeitem" aria-selected="true">London</li>
        <li id="ts-manchester" class="ts__leaf" role="treeitem" aria-selected="false">Manchester</li>
      </ul>
    </li>
    <li class="ts__branch" role="treeitem" aria-expanded="true">
      <span class="ts__branch-label">France</span>
      <ul class="ts__group" role="group">
        <li id="ts-paris" class="ts__leaf" role="treeitem" aria-selected="false">Paris</li>
      </ul>
    </li>
  </ul>
</div>
<style>
.ts { font: 14px system-ui, sans-serif; color: #241430; max-width: 260px; background: #FAF7F2; border: 1.5px solid #24143020; padding: 12px; box-sizing: border-box; }
.ts__label { font-weight: 500; margin-bottom: 8px; }
.ts__tree, .ts__group { list-style: none; margin: 0; padding: 0; }
.ts__group { padding-left: 18px; }
.ts__branch { margin-bottom: 4px; }
.ts__branch-label { display: block; padding: 6px 8px; border-radius: 6px; font-weight: 500; cursor: pointer; }
.ts__branch-label:hover { background: #24143014; }
.ts__branch[aria-expanded="false"] .ts__group { display: none; }
.ts__tree--collapsed .ts__group { display: none; }
.ts__leaf { padding: 6px 8px; border-radius: 6px; cursor: pointer; }
.ts__leaf:hover { background: #24143014; }
.ts__tree--london #ts-london, .ts__tree--manchester #ts-manchester, .ts__tree--paris #ts-paris { background: var(--accent); font-weight: 600; }
</style>

tailwind

<div class="ts-tw ts-tw--london font-sans text-sm text-[#241430] max-w-[260px] bg-[#FAF7F2] border-[1.5px] border-[#24143020] p-3 box-border" style="border-radius:8px;">
  <div class="font-medium mb-2" id="ts-tw-label">Office location</div>
  <ul class="list-none m-0 p-0" role="tree" aria-labelledby="ts-tw-label">
    <li class="mb-1" role="treeitem" aria-expanded="true">
      <span class="block px-2 py-1.5 rounded-md font-medium cursor-pointer hover:bg-[#24143014]">United Kingdom</span>
      <ul class="ts-tw-group list-none m-0 pl-[18px]" role="group">
        <li id="ts-tw-london" class="px-2 py-1.5 rounded-md cursor-pointer hover:bg-[#24143014]" role="treeitem" aria-selected="true">London</li>
        <li id="ts-tw-manchester" class="px-2 py-1.5 rounded-md cursor-pointer hover:bg-[#24143014]" role="treeitem" aria-selected="false">Manchester</li>
      </ul>
    </li>
    <li class="mb-1" role="treeitem" aria-expanded="true">
      <span class="block px-2 py-1.5 rounded-md font-medium cursor-pointer hover:bg-[#24143014]">France</span>
      <ul class="ts-tw-group list-none m-0 pl-[18px]" role="group">
        <li id="ts-tw-paris" class="px-2 py-1.5 rounded-md cursor-pointer hover:bg-[#24143014]" role="treeitem" aria-selected="false">Paris</li>
      </ul>
    </li>
  </ul>
</div>
<style>
.ts-tw--london #ts-tw-london, .ts-tw--manchester #ts-tw-manchester, .ts-tw--paris #ts-tw-paris { background: #D9C8F0; font-weight: 600; }
li[aria-expanded="false"] > .ts-tw-group { display: none; }
</style>

react

export function TreeSelect({
  label = "Office location",
  node = "london",
  expanded = true,
  accent = "#D9C8F0",
  radius = 8,
}) {
  const branches = [
    { name: "United Kingdom", children: [{ id: "london", name: "London" }, { id: "manchester", name: "Manchester" }] },
    { name: "France", children: [{ id: "paris", name: "Paris" }] },
  ];
  const leafStyle = (id) => ({
    padding: "6px 8px", borderRadius: 6, cursor: "pointer",
    background: id === node ? accent : "transparent",
    fontWeight: id === node ? 600 : 400,
  });
  return (
    <div style={{ fontFamily: "system-ui, sans-serif", fontSize: 14, color: "#241430", maxWidth: 260, background: "#FAF7F2", border: "1.5px solid #24143020", padding: 12, boxSizing: "border-box", borderRadius: radius }}>
      <div id="ts-r-label" style={{ fontWeight: 500, marginBottom: 8 }}>{label}</div>
      <ul style={{ listStyle: "none", margin: 0, padding: 0 }} role="tree" aria-labelledby="ts-r-label">
        {branches.map((branch) => (
          <li key={branch.name} style={{ marginBottom: 4 }} role="treeitem" aria-expanded={expanded}>
            <span style={{ display: "block", padding: "6px 8px", borderRadius: 6, fontWeight: 500, cursor: "pointer" }}>{branch.name}</span>
            {expanded && (
              <ul style={{ listStyle: "none", margin: 0, paddingLeft: 18 }} role="group">
                {branch.children.map((leaf) => (
                  <li key={leaf.id} role="treeitem" aria-selected={leaf.id === node} style={leafStyle(leaf.id)}>{leaf.name}</li>
                ))}
              </ul>
            )}
          </li>
        ))}
      </ul>
    </div>
  );
}