Components / Form & Input / Cascader

Cascader beta

A select that walks a tree: pick a branch, then a branch of the branch, until the leaf is the answer.

Preview

Open it and walk: Japan, then Tokyo, then the city. The trigger keeps the whole path.

Uses: Input — include its CSS alongside this one.

<div class="k-casc" data-casc style="max-width:340px">
  <button class="input" data-casc-btn type="button" aria-expanded="false" aria-haspopup="true" style="display:flex;align-items:center;justify-content:space-between;cursor:pointer;text-align:left;width:100%">
    <span data-casc-label style="color:var(--k-ink-3)">Choose a work location</span><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="square" stroke-linejoin="bevel" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
  </button>
  <div class="k-casc-panel" data-casc-panel></div>
  <div class="k-casc-src" data-casc-src aria-hidden="true"><ul>
    <li><span>Japan</span><ul>
      <li><span>Tokyo</span><ul>
        <li><span>Kiyose</span></li>
        <li><span>Tachikawa</span></li>
        <li><span>Hachioji</span></li>
      </ul></li>
      <li><span>Osaka</span><ul>
        <li><span>Osaka City</span></li>
        <li><span>Sakai</span></li>
      </ul></li>
    </ul></li>
    <li><span>Remote</span><ul>
      <li><span>Home office</span></li>
      <li><span>Client site</span></li>
    </ul></li>
  </ul></div>
</div>
<p style="font-size:12.5px;color:var(--k-ink-3);margin-top:9px">Open it and walk: Japan, then Tokyo, then the city. The trigger keeps the whole path.</p>
/* KATA Cascader — a tree, dealt into columns. */
.k-casc{position:relative;font-family:var(--k-font);color:var(--k-ink)}
.k-casc-panel{position:absolute;inset-inline-start:0;top:calc(100% + 8px);z-index:50;background:var(--k-bg);border:1px solid var(--k-line);border-radius:14px;box-shadow:0 20px 44px -14px rgba(16,24,40,.22);padding:6px;display:none}
.k-casc-panel.open{display:flex}
.k-casc-col{min-width:148px;max-height:236px;overflow:auto;padding:2px}
.k-casc-col + .k-casc-col{border-inline-start:1px solid var(--k-line)}
.k-casc-it{display:flex;align-items:center;gap:7px;width:100%;padding:7px 9px;border:0;border-radius:8px;background:none;font-family:inherit;font-size:13.5px;color:var(--k-ink-2);cursor:pointer;text-align:start}
.k-casc-it:hover{background:var(--k-bg-soft);color:var(--k-ink)}
.k-casc-it.on{background:var(--k-accent-bg);color:var(--k-ink);font-weight:550}
.k-casc-it svg{margin-inline-start:auto;color:var(--k-ink-3);flex:none}
.k-casc-src{display:none}
// KATA Cascader — the tree is nested lists in the markup; the script
// deals the current path into columns. A leaf pick writes the path.
document.querySelectorAll('[data-casc]').forEach(function (el) {
  var btn = el.querySelector('[data-casc-btn]');
  var label = el.querySelector('[data-casc-label]');
  var panel = el.querySelector('[data-casc-panel]');
  var src = el.querySelector('[data-casc-src]');
  function toData(ul) {
    return Array.prototype.map.call(ul.children, function (li) {
      var a = li.querySelector(':scope > span');
      var sub = li.querySelector(':scope > ul');
      return { label: a.textContent, children: sub ? toData(sub) : null };
    });
  }
  var tree = toData(src.querySelector('ul'));
  var path = [];
  function render() {
    panel.innerHTML = '';
    var level = tree;
    for (var d = 0; ; d++) {
      var col = document.createElement('div');
      col.className = 'k-casc-col';
      (function (nodes, depth) {
        nodes.forEach(function (n) {
          var b = document.createElement('button');
          b.type = 'button';
          b.className = 'k-casc-it' + (path[depth] === n ? ' on' : '');
          b.textContent = n.label;
          if (n.children) b.innerHTML += '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="square" stroke-linejoin="bevel" aria-hidden="true"><path d="m9 6 6 6-6 6"/></svg>';
          b.addEventListener('click', function () {
            path = path.slice(0, depth); path.push(n);
            if (n.children) { render(); }
            else {
              label.textContent = path.map(function (x) { return x.label; }).join(' / ');
              el.setAttribute('data-value', JSON.stringify(path.map(function (x) { return x.label; })));
              el.dispatchEvent(new CustomEvent('kata:cascader', { detail: path.map(function (x) { return x.label; }), bubbles: true }));
              close();
            }
          });
          col.appendChild(b);
        });
      })(level, d);
      panel.appendChild(col);
      if (path[d] && path[d].children) { level = path[d].children; } else { break; }
    }
  }
  function open() { render(); panel.classList.add('open'); btn.setAttribute('aria-expanded', 'true'); }
  function close() { panel.classList.remove('open'); btn.setAttribute('aria-expanded', 'false'); }
  btn.addEventListener('click', function () { panel.classList.contains('open') ? close() : open(); });
  document.addEventListener('click', function (e) { if (!el.contains(e.target)) close(); });
  document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
});

Installation

CLI

node kata-ui.mjs add cascader

Writes the part’s CSS and JS into your project. Dependencies are resolved and written with it. New to the CLI? See the CLI doc.

Manual

  1. Load tokens.css once, before any part.
  2. Copy the CSS tab into your stylesheet.
  3. Copy the HTML where it belongs. The part has a script — include it after the markup; it initializes itself.

Usage

For choices that are really addresses — region, category, org chart. The author writes the tree as nested lists in the panel; the script deals it into columns. The trigger shows the whole path, because the leaf alone ("Central") rarely says enough.

Accessibility

The trigger is a button with aria-expanded; columns are lists and items are buttons, the chosen path announced in the trigger text. Arrow-key column navigation is the app’s layer; Escape closes from anywhere.

Anatomy

The public surface of this part, read from its own source. Class names are the API — the CSS tab above is the full contract.

.k-cascclass
.k-casc-panelclass
.openclass
.k-casc-colclass
.k-casc-itclass
.onclass
.k-casc-srcclass
data-cascdata hook
data-casc-btndata hook
data-casc-labeldata hook
data-casc-paneldata hook
data-casc-srcdata hook
data-valuedata hook
aria-expandedARIA
aria-haspopupARIA
aria-hiddenARIA

Related parts