Components / Form & Input / Transfer
Transfer stable
Two lists and a shuttle: what is in, what is out, and the move between them, counted.
Uses: Button, Input — include its CSS alongside this one.
<div class="k-xfer" data-xfer>
<div class="k-xfer-panel" data-xfer-l>
<div class="k-xfer-h"><input type="checkbox" data-xfer-all aria-label="Select all in Catalog"> Catalog <span class="cnt" data-xfer-cnt></span></div>
<div class="k-xfer-filter searchwrap"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="square" stroke-linejoin="bevel" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.5-4.5"/></svg><input class="input" data-xfer-q type="search" placeholder="Filter" aria-label="Filter Catalog" style="height:32px;font-size:12.5px"></div>
<div class="k-xfer-list"><label class="k-xfer-it"><input type="checkbox"> Button</label><label class="k-xfer-it"><input type="checkbox"> Calendar</label><label class="k-xfer-it"><input type="checkbox"> Data Table</label><label class="k-xfer-it"><input type="checkbox"> Sidebar</label><label class="k-xfer-it"><input type="checkbox"> Spotlight</label><label class="k-xfer-it"><input type="checkbox"> Tree</label></div>
</div>
<div class="k-xfer-acts">
<button class="btn btn-sec btn-md" type="button" data-xfer-tor aria-label="Move selected right">></button>
<button class="btn btn-sec btn-md" type="button" data-xfer-tol aria-label="Move selected left"><</button>
</div>
<div class="k-xfer-panel" data-xfer-r>
<div class="k-xfer-h"><input type="checkbox" data-xfer-all aria-label="Select all in On this page"> On this page <span class="cnt" data-xfer-cnt></span></div>
<div class="k-xfer-filter searchwrap"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="square" stroke-linejoin="bevel" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.5-4.5"/></svg><input class="input" data-xfer-q type="search" placeholder="Filter" aria-label="Filter On this page" style="height:32px;font-size:12.5px"></div>
<div class="k-xfer-list"><label class="k-xfer-it"><input type="checkbox"> Badge</label><label class="k-xfer-it"><input type="checkbox"> Card</label><label class="k-xfer-it"><input type="checkbox"> Tabs</label></div>
</div>
</div>/* KATA Transfer — two lists and a shuttle. */
.k-xfer{width:100%;display:grid;grid-template-columns:1fr auto 1fr;gap:12px;align-items:stretch;font-family:var(--k-font);color:var(--k-ink);max-width:560px}
.k-xfer-panel{border:1px solid var(--k-line);border-radius:var(--k-radius-lg);display:flex;flex-direction:column;background:var(--k-bg);min-height:216px}
.k-xfer-h{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid var(--k-line);font-size:12.5px;font-weight:600}
.k-xfer-h .cnt{margin-inline-start:auto;color:var(--k-ink-3);font-weight:500}
.k-xfer-filter{margin:9px 10px 4px}
.k-xfer-list{padding:5px;overflow:auto;display:flex;flex-direction:column;gap:1px}
.k-xfer-it{display:flex;align-items:center;gap:9px;padding:7px 9px;border-radius:8px;font-size:13px;cursor:pointer}
.k-xfer-it:hover{background:var(--k-bg-soft)}
.k-xfer input[type=checkbox]{accent-color:var(--k-ink);width:15px;height:15px;flex:none;cursor:pointer}
.k-xfer-acts{display:flex;flex-direction:column;gap:8px;justify-content:center}
.k-xfer-acts .btn{padding:0 11px}
@media (max-width:560px){.k-xfer{grid-template-columns:1fr}.k-xfer-acts{flex-direction:row;justify-content:center}}
// KATA Transfer — checked items move; counts and filters stay honest.
document.querySelectorAll('[data-xfer]').forEach(function (root) {
var sides = { l: root.querySelector('[data-xfer-l]'), r: root.querySelector('[data-xfer-r]') };
function items(side) { return Array.prototype.slice.call(side.querySelectorAll('.k-xfer-it')); }
function refresh() {
Object.keys(sides).forEach(function (k) {
var s = sides[k], list = items(s);
var checked = list.filter(function (i) { return i.querySelector('input').checked; }).length;
s.querySelector('[data-xfer-cnt]').textContent = list.length + ' items';
var all = s.querySelector('[data-xfer-all]');
all.checked = list.length > 0 && checked === list.length;
all.indeterminate = checked > 0 && checked < list.length;
});
root.querySelector('[data-xfer-tor]').disabled = !items(sides.l).some(function (i) { return i.querySelector('input').checked; });
root.querySelector('[data-xfer-tol]').disabled = !items(sides.r).some(function (i) { return i.querySelector('input').checked; });
}
function move(from, to) {
items(from).forEach(function (i) {
var box = i.querySelector('input');
if (box.checked) { box.checked = false; to.querySelector('.k-xfer-list').appendChild(i); }
});
refresh();
}
root.querySelector('[data-xfer-tor]').addEventListener('click', function () { move(sides.l, sides.r); });
root.querySelector('[data-xfer-tol]').addEventListener('click', function () { move(sides.r, sides.l); });
Object.keys(sides).forEach(function (k) {
var s = sides[k];
s.addEventListener('change', refresh);
s.querySelector('[data-xfer-all]').addEventListener('change', function (e) {
items(s).forEach(function (i) { if (i.style.display !== 'none') i.querySelector('input').checked = e.target.checked; });
refresh();
});
s.querySelector('[data-xfer-q]').addEventListener('input', function (e) {
var q = e.target.value.toLowerCase();
items(s).forEach(function (i) { i.style.display = i.textContent.toLowerCase().indexOf(q) !== -1 ? '' : 'none'; });
});
});
refresh();
});
Installation
CLI
node kata-ui.mjs add transfer
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 assigning things in bulk — members to a team, parts to a page, tags to a post. Each side filters itself, the buttons move only what is checked, and the counts always say how many are where.
Accessibility
Real checkboxes in two labelled groups; the move buttons say what they move ("Move selected right"), and the header checkbox per side selects that side only. Moving focus is left to the reader — nothing jumps.
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-xfer | class |
.k-xfer-panel | class |
.k-xfer-h | class |
.cnt | class |
.k-xfer-filter | class |
.k-xfer-list | class |
.k-xfer-it | class |
.k-xfer-acts | class |
.btn | class |
data-xfer | data hook |
data-xfer-l | data hook |
data-xfer-all | data hook |
data-xfer-cnt | data hook |
data-xfer-q | data hook |
data-xfer-tor | data hook |
data-xfer-tol | data hook |
data-xfer-r | data hook |
aria-label | 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.
Cascader beta
A select that walks a tree: pick a branch, then a branch of the branch, until the leaf is the answer.