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.
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
- Load
tokens.cssonce, before any part. - Copy the CSS tab into your stylesheet.
- 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-casc | class |
.k-casc-panel | class |
.open | class |
.k-casc-col | class |
.k-casc-it | class |
.on | class |
.k-casc-src | class |
data-casc | data hook |
data-casc-btn | data hook |
data-casc-label | data hook |
data-casc-panel | data hook |
data-casc-src | data hook |
data-value | data hook |
aria-expanded | ARIA |
aria-haspopup | ARIA |
aria-hidden | ARIA |
Related parts
Button stable
Black leads; lines and ghosts support. Hover, disabled and loading states included.
Button Group stable
Buttons joined into one piece: shared edges, one radius, no gaps. For toolbars, paging and split actions.
Calendar stable
One month, standing on its own: a grid of real day buttons with today marked and a selection you can move.
Checkbox / Radio stable
Native inputs with a restyled face. Keyboard support and form behavior stay intact.