Components / Form & Input / Transfer

Transfer stable

Two lists and a shuttle: what is in, what is out, and the move between them, counted.

Preview
Catalog
On this page

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">&gt;</button>
    <button class="btn btn-sec btn-md" type="button" data-xfer-tol aria-label="Move selected left">&lt;</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

  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 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-xferclass
.k-xfer-panelclass
.k-xfer-hclass
.cntclass
.k-xfer-filterclass
.k-xfer-listclass
.k-xfer-itclass
.k-xfer-actsclass
.btnclass
data-xferdata hook
data-xfer-ldata hook
data-xfer-alldata hook
data-xfer-cntdata hook
data-xfer-qdata hook
data-xfer-tordata hook
data-xfer-toldata hook
data-xfer-rdata hook
aria-labelARIA
aria-hiddenARIA

Related parts