Open demo

CSS code

HTML
<div><span class="lbl">Assign people to teams</span>
<section class="as" data-dd="wrap" aria-labelledby="as-title">
  <div class="as-panel" data-dd-zone>
    <div class="as-head"><h2 id="as-title">Autumn hackathon teams</h2><p id="as-hint">Drag a person onto a team, or focus their handle, press Space, then use Left and Right to change team.</p></div>
    <div class="as-th"><h3>Unassigned</h3><span class="as-cap"><span data-dd-count="as-pool">3</span> left</span></div>
    <ul class="as-drop" id="as-pool" data-dd-list="people" aria-label="Unassigned">
      <li class="as-chip" data-dd-item><button class="as-handle" type="button" data-dd-handle aria-describedby="as-hint"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="as-av" style="background:#fef6ee;color:#b93815" aria-hidden="true">CM</span><span class="as-who"><b data-dd-name>Chloé Martin</b><small>UX research</small></span></li>
      <li class="as-chip" data-dd-item><button class="as-handle" type="button" data-dd-handle aria-describedby="as-hint"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="as-av" style="background:#f0f9ff;color:#026aa2" aria-hidden="true">FW</span><span class="as-who"><b data-dd-name>Felix Wu</b><small>Data analyst</small></span></li>
      <li class="as-chip" data-dd-item><button class="as-handle" type="button" data-dd-handle aria-describedby="as-hint"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="as-av" style="background:#f2f4f7;color:#344054" aria-hidden="true">HS</span><span class="as-who"><b data-dd-name>Hugo Silva</b><small>Content design</small></span></li>
    </ul>
  </div>
  <div class="as-teams">
    <div class="as-team" data-dd-zone><div class="as-th"><h3>Design</h3><span class="as-cap"><span data-dd-count="as-design">2</span> / 3<span class="sr-only"> people</span></span></div>
      <ul class="as-drop" id="as-design" data-dd-list="people" data-dd-max="3" aria-label="Design">
        <li class="as-chip" data-dd-item><button class="as-handle" type="button" data-dd-handle aria-describedby="as-hint"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="as-av" style="background:#fdf2fa;color:#9e165f" aria-hidden="true">AR</span><span class="as-who"><b data-dd-name>Ana Ruiz</b><small>Illustrator</small></span></li>
        <li class="as-chip" data-dd-item><button class="as-handle" type="button" data-dd-handle aria-describedby="as-hint"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="as-av" style="background:#f4f3ff;color:#5925dc" aria-hidden="true">EN</span><span class="as-who"><b data-dd-name>Emma Novak</b><small>Product design</small></span></li>
      </ul></div>
    <div class="as-team" data-dd-zone><div class="as-th"><h3>Engineering</h3><span class="as-cap"><span data-dd-count="as-eng">3</span> / 4<span class="sr-only"> people</span></span></div>
      <ul class="as-drop" id="as-eng" data-dd-list="people" data-dd-max="4" aria-label="Engineering">
        <li class="as-chip" data-dd-item><button class="as-handle" type="button" data-dd-handle aria-describedby="as-hint"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="as-av" style="background:#eef4ff;color:#3538cd" aria-hidden="true">BC</span><span class="as-who"><b data-dd-name>Ben Carter</b><small>Front-end</small></span></li>
        <li class="as-chip" data-dd-item><button class="as-handle" type="button" data-dd-handle aria-describedby="as-hint"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="as-av" style="background:#ecfdf3;color:#067647" aria-hidden="true">DP</span><span class="as-who"><b data-dd-name>Dev Patel</b><small>Back-end</small></span></li>
        <li class="as-chip" data-dd-item><button class="as-handle" type="button" data-dd-handle aria-describedby="as-hint"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="as-av" style="background:#fefbe8;color:#a15c07" aria-hidden="true">GL</span><span class="as-who"><b data-dd-name>Grace Liu</b><small>QA</small></span></li>
      </ul></div>
    <div class="as-team" data-dd-zone><div class="as-th"><h3>Research</h3><span class="as-cap"><span data-dd-count="as-research">0</span> / 2<span class="sr-only"> people</span></span></div>
      <ul class="as-drop" id="as-research" data-dd-list="people" data-dd-max="2" aria-label="Research">
      </ul></div>
  </div>
  <p class="sr-only" aria-live="assertive" aria-atomic="true" data-dd-live></p>
</section></div>

<div><span class="lbl">Plan a sprint against its capacity</span>
<section class="as as-panel" data-dd aria-labelledby="sp-title">
  <div class="as-head"><h2 id="sp-title">Sprint 15 · Oct 21 to Nov 1</h2><p id="sp-hint">Drag stories into the sprint, or focus a handle, press Space and use the arrow keys. Left and Right switch lists.</p></div>
  <div class="sp-cols">
    <div data-dd-zone><div class="as-th"><h3>Backlog</h3><span class="as-cap"><span data-dd-count="sp-backlog">4</span> stories</span></div>
      <ul class="sp-list" id="sp-backlog" data-dd-list="stories" aria-label="Backlog">
        <li class="sp-row" data-dd-item data-points="5"><button class="as-handle" type="button" data-dd-handle aria-describedby="sp-hint"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="sp-title" data-dd-name>Password reset flow</span><span class="sp-tag" style="background:#f4f3ff;color:#5925dc">Feature</span><span class="sp-pts" role="img" aria-label="5 points">5</span></li>
        <li class="sp-row" data-dd-item data-points="3"><button class="as-handle" type="button" data-dd-handle aria-describedby="sp-hint"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="sp-title" data-dd-name>CSV export for invoices</span><span class="sp-tag" style="background:#f4f3ff;color:#5925dc">Feature</span><span class="sp-pts" role="img" aria-label="3 points">3</span></li>
        <li class="sp-row" data-dd-item data-points="8"><button class="as-handle" type="button" data-dd-handle aria-describedby="sp-hint"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="sp-title" data-dd-name>Onboarding checklist</span><span class="sp-tag" style="background:#f4f3ff;color:#5925dc">Feature</span><span class="sp-pts" role="img" aria-label="8 points">8</span></li>
        <li class="sp-row" data-dd-item data-points="2"><button class="as-handle" type="button" data-dd-handle aria-describedby="sp-hint"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="sp-title" data-dd-name>Fix timezone bug in reports</span><span class="sp-tag" style="background:#fef3f2;color:#b42318">Bug</span><span class="sp-pts" role="img" aria-label="2 points">2</span></li>
      </ul></div>
    <div data-dd-zone><div class="as-th"><h3>Sprint 15</h3><span class="as-cap"><span data-dd-count="sp-sprint">3</span> stories</span></div>
      <div class="sp-meter" aria-hidden="true"><span data-sp-bar style="width:65%"></span></div>
      <p class="sp-sum"><span><strong data-sp-points>13</strong> of 20 points</span><span data-sp-note>7 points free</span></p>
      <ul class="sp-list" id="sp-sprint" data-dd-list="stories" data-capacity="20" aria-label="Sprint 15">
        <li class="sp-row" data-dd-item data-points="3"><button class="as-handle" type="button" data-dd-handle aria-describedby="sp-hint"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="sp-title" data-dd-name>Dark mode for settings</span><span class="sp-tag" style="background:#f4f3ff;color:#5925dc">Feature</span><span class="sp-pts" role="img" aria-label="3 points">3</span></li>
        <li class="sp-row" data-dd-item data-points="5"><button class="as-handle" type="button" data-dd-handle aria-describedby="sp-hint"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="sp-title" data-dd-name>Audit log page</span><span class="sp-tag" style="background:#f4f3ff;color:#5925dc">Feature</span><span class="sp-pts" role="img" aria-label="5 points">5</span></li>
        <li class="sp-row" data-dd-item data-points="5"><button class="as-handle" type="button" data-dd-handle aria-describedby="sp-hint"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="sp-title" data-dd-name>Two-factor sign-in</span><span class="sp-tag" style="background:#ecfdf3;color:#067647">Security</span><span class="sp-pts" role="img" aria-label="5 points">5</span></li>
      </ul></div>
  </div>
  <p class="sr-only" aria-live="assertive" aria-atomic="true" data-dd-live></p>
</section></div>
CSS
/* Drag into targets: every target is a <ul data-dd-list> in the same group, so people and stories can move
   between them; data-dd-max caps a target, and a full target refuses drops (red) and is announced as full. */
.as{--as-accent:#6d28d9;--as-tint:#f4efff;--as-line:#e4e7ec;--as-muted:#5d6679;--as-ink:#101828;--as-danger:#b42318;color:var(--as-ink)}
.as-panel{background:#fff;border:1px solid var(--as-line);border-radius:16px;padding:16px;box-shadow:0 1px 2px rgba(16,24,40,.05),0 10px 28px -16px rgba(16,24,40,.18)}
.as-head{display:flex;align-items:baseline;justify-content:space-between;gap:6px 16px;flex-wrap:wrap;margin-bottom:12px}
.as-head h2{font-size:1rem;letter-spacing:-.01em}
.as-head p{font-size:.82rem;color:var(--as-muted)}
.as-th{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:10px}
.as-th h3{font-size:.86rem}
.as-cap{font-size:.75rem;font-weight:700;padding:1px 8px;border-radius:999px;background:#f2f4f7;color:#344054;font-variant-numeric:tabular-nums}
.as-team:has(> [data-full]) .as-cap{background:#fef3f2;color:var(--as-danger)}
.as-teams{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin-top:12px}
.as-team{background:#f8f9fb;border:1px solid var(--as-line);border-radius:14px;padding:12px}
.as-drop{list-style:none;display:flex;flex-wrap:wrap;align-content:flex-start;gap:8px;min-height:52px;padding:4px;border-radius:12px;outline:2px dashed transparent;outline-offset:0;transition:background-color .15s,outline-color .15s}
.as-drop[data-empty]::after{content:"Drop people here";flex:1;display:grid;place-items:center;min-height:44px;border:2px dashed #b3bbc8;border-radius:10px;font-size:.8rem;color:var(--as-muted)}
.as[data-dd-active] .as-drop{outline-color:#c3c9d4}
.as .as-drop[data-over]{outline-color:var(--as-accent);background:var(--as-tint)}
.as .as-drop[data-over][data-full]:not(:has([data-dragging],[data-grabbed])){outline-color:var(--as-danger);background:#fef3f2}
.as-chip{display:flex;align-items:center;gap:8px;padding:4px 12px 4px 4px;background:#fff;border:1px solid #d0d5dd;border-radius:999px;cursor:grab;user-select:none}
.as-handle{display:grid;place-items:center;width:26px;height:30px;border:0;border-radius:999px;background:none;color:#667085;cursor:grab;touch-action:none}
.as-handle:hover{background:#f2f4f7;color:var(--as-ink)}
.as-handle:focus-visible{outline:2px solid var(--as-accent);outline-offset:1px}
.as-handle svg{width:16px;height:16px}
.as-av{display:grid;place-items:center;width:28px;height:28px;border-radius:50%;font-size:.66rem;font-weight:800}
.as-who{display:grid;line-height:1.2}
.as-who b{font-size:.84rem;font-weight:600}
.as-who small{font-size:.72rem;color:var(--as-muted)}
.sp-cols{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.sp-list{list-style:none;display:grid;align-content:start;gap:6px;min-height:120px;padding:4px;border-radius:12px;outline:2px dashed transparent;transition:background-color .15s,outline-color .15s}
.sp-list[data-empty]::after{content:"Drop stories here";display:grid;place-items:center;min-height:100px;border:2px dashed #b3bbc8;border-radius:10px;font-size:.8rem;color:var(--as-muted)}
.as[data-dd-active] .sp-list{outline-color:#c3c9d4}
.as .sp-list[data-over]{outline-color:var(--as-accent);background:var(--as-tint)}
.sp-row{display:flex;align-items:center;gap:8px;padding:8px 10px 8px 6px;background:#fff;border:1px solid var(--as-line);border-radius:10px;cursor:grab;user-select:none}
.sp-row .as-handle{border-radius:7px}
.sp-title{flex:1;min-width:0;font-size:.86rem;font-weight:600}
.sp-tag{font-size:.68rem;font-weight:700;padding:2px 7px;border-radius:6px}
.sp-pts{display:grid;place-items:center;min-width:26px;height:26px;border-radius:50%;background:#f2f4f7;font-size:.75rem;font-weight:800;color:#344054}
.sp-meter{height:8px;margin:2px 0 6px;border-radius:999px;background:#ece6fb;overflow:hidden}
.sp-meter span{display:block;height:100%;border-radius:inherit;background:var(--as-accent);transition:width .25s}
.sp-sum{display:flex;justify-content:space-between;gap:8px;font-size:.78rem;color:var(--as-muted);margin-bottom:8px}
.sp-sum strong{color:var(--as-ink)}
.as[data-over-capacity] .sp-meter span{background:#b54708}
.as[data-over-capacity] [data-sp-note]{color:#b54708;font-weight:700}
/* states set by the script */
.as-chip[data-dragging],.sp-row[data-dragging]{background:var(--as-tint);border:2px dashed #b9a2f5}
.as-chip[data-dragging]>*,.sp-row[data-dragging]>*{visibility:hidden}
.as-chip[data-dd-ghost],.sp-row[data-dd-ghost]{box-shadow:0 16px 32px -10px rgba(16,24,40,.4);rotate:-2deg}
.as-chip[data-grabbed],.sp-row[data-grabbed]{border-color:var(--as-accent);box-shadow:0 0 0 2px var(--as-accent),0 10px 20px -10px rgba(109,40,217,.6)}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
@media (max-width:560px){.sp-cols{grid-template-columns:1fr}}
@media (prefers-reduced-motion:reduce){.as-drop,.sp-list,.sp-meter span{transition:none}.as-chip[data-dd-ghost],.sp-row[data-dd-ghost]{rotate:none}}
JavaScript
// Sprint planner: total the story points in the sprint list and compare them with its capacity.
document.addEventListener('dd:change', e => {
  const root = e.target, list = root.querySelector('[data-capacity]'); if (!list) return;
  const pts = [...list.querySelectorAll('[data-dd-item]')].reduce((s, li) => s + +li.dataset.points, 0), cap = +list.dataset.capacity;
  root.querySelector('[data-sp-points]').textContent = pts;
  root.querySelector('[data-sp-bar]').style.width = Math.min(100, pts / cap * 100) + '%';
  root.querySelector('[data-sp-note]').textContent = pts > cap ? 'Over capacity by ' + (pts - cap) + (pts - cap === 1 ? ' point' : ' points') : (cap - pts) + ' points free';
  root.toggleAttribute('data-over-capacity', pts > cap);
});
// Drag and drop with pointer events (mouse, touch and pen) plus a keyboard mode on every handle:
// Space or Enter picks an item up, the arrow keys move it, Space or Enter drops it, Escape puts it back.
// Every move is announced in the component's aria-live region. Markup contract: [data-dd] root,
// [data-dd-list="group"] lists, [data-dd-item] items with a [data-dd-name] and a [data-dd-handle] button.
// data-dd="grid" or "wrap" compares the pointer horizontally; in a grid, Up and Down jump a whole row.
document.querySelectorAll('[data-dd]').forEach(root => {
  const grid = root.dataset.dd === 'grid', horiz = grid || root.dataset.dd === 'wrap', live = root.querySelector('[data-dd-live]');
  const items = l => [...l.children].filter(c => c.hasAttribute('data-dd-item'));
  const lists = l => [...root.querySelectorAll(`[data-dd-list="${l.dataset.ddList}"]`)];
  const name = el => el.querySelector('[data-dd-name]').textContent.trim();
  const full = l => !!l.dataset.ddMax && items(l).length >= +l.dataset.ddMax;
  const pos = el => { const l = el.parentElement, all = items(l);
    return (lists(l).length > 1 ? l.getAttribute('aria-label') + ', ' : '') + 'position ' + (all.indexOf(el) + 1) + ' of ' + all.length; };
  const say = msg => { live.textContent = ''; setTimeout(() => { live.textContent = msg; }, 60); };
  const clearOver = keep => root.querySelectorAll('[data-over]').forEach(l => l !== keep && l.removeAttribute('data-over'));
  const marks = () => root.querySelectorAll('[data-dd-list]').forEach(l => { const n = items(l).length, c = root.querySelector(`[data-dd-count="${l.id}"]`);
    l.toggleAttribute('data-empty', !n); l.toggleAttribute('data-full', full(l)); if (c) c.textContent = n; });
  const changed = () => {
    marks();
    root.querySelectorAll('[data-dd-item]').forEach(it => it.querySelector('[data-dd-handle]').setAttribute('aria-label', 'Move ' + name(it)));
    root.dispatchEvent(new Event('dd:change', { bubbles: true }));
  };
  let drag, ghost, home, kb, kbh, sx, sy, ox, oy, pid, dragged, moving;
  const putBack = el => home[0].insertBefore(el, home[1] && home[1].parentNode === home[0] ? home[1] : null);

  root.addEventListener('pointerdown', e => {
    const item = e.target.closest('[data-dd-item]'), handle = e.target.closest('[data-dd-handle]');
    if (!item || e.button > 0 || kb) return;
    if (!handle && (e.pointerType !== 'mouse' || 'ddHandleOnly' in root.dataset || e.target.closest('a,button,input,select,textarea,label'))) return;
    e.preventDefault(); drag = item; pid = e.pointerId; sx = e.clientX; sy = e.clientY; ghost = null;
  });
  root.addEventListener('pointermove', e => {
    if (!drag || e.pointerId !== pid) return;
    if (!ghost) { if (Math.hypot(e.clientX - sx, e.clientY - sy) < 6) return; start(); root.setPointerCapture(pid); }
    ghost.style.translate = `${e.clientX - ox}px ${e.clientY - oy}px`;
    if (e.clientY < 40) scrollBy(0, -12); else if (e.clientY > innerHeight - 40) scrollBy(0, 12);
    const hit = document.elementFromPoint(e.clientX, e.clientY), sel = `[data-dd-list="${home[0].dataset.ddList}"]`;
    const list = hit && (hit.closest(sel) || hit.closest('[data-dd-zone]')?.querySelector(sel)); // a [data-dd-zone] wrapper widens the target
    clearOver(list);
    if (!list || !root.contains(list) || drag.contains(list)) return;
    list.setAttribute('data-over', '');
    if (list !== drag.parentElement && full(list)) return;
    const over = items(list).find(c => c !== drag && c.contains(hit));
    if (over) { const r = over.getBoundingClientRect();
      const ref = (horiz ? e.clientX > r.left + r.width / 2 : e.clientY > r.top + r.height / 2) ? over.nextSibling : over;
      if (ref !== drag && ref !== drag.nextSibling) { list.insertBefore(drag, ref); marks(); }
    } else if (list !== drag.parentElement && !drag.contains(hit)) { list.append(drag); marks(); }
  });
  const end = e => {
    if (!drag || e.pointerId !== pid) return;
    if (ghost) {
      ghost.remove(); drag.removeAttribute('data-dragging'); root.removeAttribute('data-dd-active'); document.body.style.cursor = ''; clearOver();
      if (e.type === 'pointercancel') putBack(drag); else say(name(drag) + ' moved to ' + pos(drag) + '.');
      changed(); dragged = true; setTimeout(() => { dragged = false; });
    }
    drag = null;
  };
  root.addEventListener('pointerup', end); root.addEventListener('pointercancel', end);
  function start() {
    const r = drag.getBoundingClientRect(); ox = sx - r.left; oy = sy - r.top; home = [drag.parentElement, drag.nextSibling];
    ghost = drag.cloneNode(true); ghost.removeAttribute('data-dd-item'); ghost.setAttribute('data-dd-ghost', ''); ghost.setAttribute('aria-hidden', 'true'); ghost.inert = true;
    ghost.querySelectorAll('[id]').forEach(n => n.removeAttribute('id'));
    Object.assign(ghost.style, { position: 'fixed', left: 0, top: 0, width: r.width + 'px', height: r.height + 'px', margin: 0, zIndex: 999, pointerEvents: 'none' });
    drag.after(ghost); drag.setAttribute('data-dragging', ''); root.setAttribute('data-dd-active', ''); document.body.style.cursor = 'grabbing';
  }

  // Keyboard and screen reader mode: the handle is a toggle button (aria-pressed) that picks the item up.
  const finish = msg => { kb.removeAttribute('data-grabbed'); kbh.setAttribute('aria-pressed', 'false'); root.removeAttribute('data-dd-active'); clearOver(); kb = null; changed(); say(msg); };
  const move = fn => { moving = true; fn(); kbh.focus(); moving = false; kbh.scrollIntoView({ block: 'nearest' }); clearOver(kb.parentElement); kb.parentElement.setAttribute('data-over', ''); changed(); say(name(kb) + ' moved to ' + pos(kb) + '.'); };
  root.addEventListener('click', e => {
    const h = e.target.closest('[data-dd-handle]'); if (!h || dragged) return;
    const item = h.closest('[data-dd-item]');
    if (kb === item) return finish(name(item) + ' dropped, ' + pos(item) + '.');
    if (kb) finish(name(kb) + ' dropped, ' + pos(kb) + '.');
    kb = item; kbh = h; home = [item.parentElement, item.nextSibling];
    item.setAttribute('data-grabbed', ''); h.setAttribute('aria-pressed', 'true'); root.setAttribute('data-dd-active', ''); item.parentElement.setAttribute('data-over', ''); h.focus();
    say(name(item) + ' picked up, ' + pos(item) + '. Use the arrow keys to move it, Space to drop it, Escape to cancel.');
  });
  root.addEventListener('keydown', e => {
    if (!kb || e.target !== kbh) return;
    if (e.key === 'Escape') { e.preventDefault(); moving = true; putBack(kb); kbh.focus(); moving = false; return finish('Move cancelled. ' + name(kb) + ' returned to ' + pos(kb) + '.'); }
    const d = { ArrowUp: -1, ArrowLeft: -1, ArrowDown: 1, ArrowRight: 1 }[e.key]; if (!d) return;
    e.preventDefault();
    const l = kb.parentElement, all = items(l), i = all.indexOf(kb), ls = lists(l), side = e.key === 'ArrowLeft' || e.key === 'ArrowRight';
    if (side && ls.length > 1) {
      const t = ls[ls.indexOf(l) + d];
      if (!t) return say(name(kb) + ' is already in ' + l.getAttribute('aria-label') + ', the ' + (d < 0 ? 'first' : 'last') + ' list.');
      if (full(t)) return say(t.getAttribute('aria-label') + ' is full.');
      return move(() => t.insertBefore(kb, items(t)[i] || null));
    }
    const step = grid && !side ? d * all.filter(x => x.offsetTop === all[0].offsetTop).length : d, j = Math.max(0, Math.min(all.length - 1, i + step));
    if (j === i) return say(name(kb) + ' is already ' + (d < 0 ? 'first' : 'last') + '.');
    move(() => l.insertBefore(kb, j > i ? all[j].nextSibling : all[j]));
  });
  root.addEventListener('focusout', e => { if (kb && !moving && e.target === kbh) finish(name(kb) + ' dropped, ' + pos(kb) + '.'); });
  changed();
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Assign people to teams</span>
<section class="as" data-dd="wrap" aria-labelledby="as-title">
  <p class="visually-hidden" aria-live="assertive" aria-atomic="true" data-dd-live></p>
  <div class="as-panel card" data-dd-zone>
    <div class="as-head d-flex flex-wrap align-items-baseline justify-content-between column-gap-3 row-gap-1 mb-3"><h2 class="fw-bold mb-0" id="as-title">Autumn hackathon teams</h2><p class="text-body-secondary mb-0" id="as-hint">Drag a person onto a team, or focus their handle, press Space, then use Left and Right to change team.</p></div>
    <div class="d-flex align-items-center justify-content-between gap-2 mb-2"><h3 class="fw-bold mb-0">Unassigned</h3><span class="as-cap badge rounded-pill"><span data-dd-count="as-pool">3</span> left</span></div>
    <ul class="as-drop d-flex flex-wrap gap-2 list-unstyled mb-0" id="as-pool" data-dd-list="people" aria-label="Unassigned">
      <li class="as-chip d-flex align-items-center gap-2 rounded-pill" data-dd-item><button class="as-handle btn d-grid" type="button" data-dd-handle aria-describedby="as-hint"><svg class="m-auto" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="as-av rounded-circle d-grid align-content-center text-center" style="background:#fef6ee;color:#b93815" aria-hidden="true">CM</span><span class="as-who d-grid"><b data-dd-name>Chloé Martin</b><small class="text-body-secondary">UX research</small></span></li>
      <li class="as-chip d-flex align-items-center gap-2 rounded-pill" data-dd-item><button class="as-handle btn d-grid" type="button" data-dd-handle aria-describedby="as-hint"><svg class="m-auto" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="as-av rounded-circle d-grid align-content-center text-center" style="background:#f0f9ff;color:#026aa2" aria-hidden="true">FW</span><span class="as-who d-grid"><b data-dd-name>Felix Wu</b><small class="text-body-secondary">Data analyst</small></span></li>
      <li class="as-chip d-flex align-items-center gap-2 rounded-pill" data-dd-item><button class="as-handle btn d-grid" type="button" data-dd-handle aria-describedby="as-hint"><svg class="m-auto" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="as-av rounded-circle d-grid align-content-center text-center" style="background:#f2f4f7;color:#344054" aria-hidden="true">HS</span><span class="as-who d-grid"><b data-dd-name>Hugo Silva</b><small class="text-body-secondary">Content design</small></span></li>
    </ul>
  </div>
  <div class="row row-cols-1 row-cols-sm-3 g-3 mt-0">
    <div class="col"><div class="as-team card h-100" data-dd-zone><div class="d-flex align-items-center justify-content-between gap-2 mb-2"><h3 class="fw-bold mb-0">Design</h3><span class="as-cap badge rounded-pill"><span data-dd-count="as-design">2</span> / 3<span class="visually-hidden"> people</span></span></div>
      <ul class="as-drop d-flex flex-wrap gap-2 list-unstyled mb-0" id="as-design" data-dd-list="people" data-dd-max="3" aria-label="Design">
        <li class="as-chip d-flex align-items-center gap-2 rounded-pill" data-dd-item><button class="as-handle btn d-grid" type="button" data-dd-handle aria-describedby="as-hint"><svg class="m-auto" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="as-av rounded-circle d-grid align-content-center text-center" style="background:#fdf2fa;color:#9e165f" aria-hidden="true">AR</span><span class="as-who d-grid"><b data-dd-name>Ana Ruiz</b><small class="text-body-secondary">Illustrator</small></span></li>
        <li class="as-chip d-flex align-items-center gap-2 rounded-pill" data-dd-item><button class="as-handle btn d-grid" type="button" data-dd-handle aria-describedby="as-hint"><svg class="m-auto" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="as-av rounded-circle d-grid align-content-center text-center" style="background:#f4f3ff;color:#5925dc" aria-hidden="true">EN</span><span class="as-who d-grid"><b data-dd-name>Emma Novak</b><small class="text-body-secondary">Product design</small></span></li>
      </ul></div></div>
    <div class="col"><div class="as-team card h-100" data-dd-zone><div class="d-flex align-items-center justify-content-between gap-2 mb-2"><h3 class="fw-bold mb-0">Engineering</h3><span class="as-cap badge rounded-pill"><span data-dd-count="as-eng">3</span> / 4<span class="visually-hidden"> people</span></span></div>
      <ul class="as-drop d-flex flex-wrap gap-2 list-unstyled mb-0" id="as-eng" data-dd-list="people" data-dd-max="4" aria-label="Engineering">
        <li class="as-chip d-flex align-items-center gap-2 rounded-pill" data-dd-item><button class="as-handle btn d-grid" type="button" data-dd-handle aria-describedby="as-hint"><svg class="m-auto" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="as-av rounded-circle d-grid align-content-center text-center" style="background:#eef4ff;color:#3538cd" aria-hidden="true">BC</span><span class="as-who d-grid"><b data-dd-name>Ben Carter</b><small class="text-body-secondary">Front-end</small></span></li>
        <li class="as-chip d-flex align-items-center gap-2 rounded-pill" data-dd-item><button class="as-handle btn d-grid" type="button" data-dd-handle aria-describedby="as-hint"><svg class="m-auto" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="as-av rounded-circle d-grid align-content-center text-center" style="background:#ecfdf3;color:#067647" aria-hidden="true">DP</span><span class="as-who d-grid"><b data-dd-name>Dev Patel</b><small class="text-body-secondary">Back-end</small></span></li>
        <li class="as-chip d-flex align-items-center gap-2 rounded-pill" data-dd-item><button class="as-handle btn d-grid" type="button" data-dd-handle aria-describedby="as-hint"><svg class="m-auto" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="as-av rounded-circle d-grid align-content-center text-center" style="background:#fefbe8;color:#a15c07" aria-hidden="true">GL</span><span class="as-who d-grid"><b data-dd-name>Grace Liu</b><small class="text-body-secondary">QA</small></span></li>
      </ul></div></div>
    <div class="col"><div class="as-team card h-100" data-dd-zone><div class="d-flex align-items-center justify-content-between gap-2 mb-2"><h3 class="fw-bold mb-0">Research</h3><span class="as-cap badge rounded-pill"><span data-dd-count="as-research">0</span> / 2<span class="visually-hidden"> people</span></span></div>
      <ul class="as-drop d-flex flex-wrap gap-2 list-unstyled mb-0" id="as-research" data-dd-list="people" data-dd-max="2" aria-label="Research">
      </ul></div></div>
  </div>
</section></div>

<div><span class="lbl">Plan a sprint against its capacity</span>
<section class="as" data-dd aria-labelledby="sp-title">
  <p class="visually-hidden" aria-live="assertive" aria-atomic="true" data-dd-live></p>
  <div class="as-panel card">
  <div class="as-head d-flex flex-wrap align-items-baseline justify-content-between column-gap-3 row-gap-1 mb-3"><h2 class="fw-bold mb-0" id="sp-title">Sprint 15 · Oct 21 to Nov 1</h2><p class="text-body-secondary mb-0" id="sp-hint">Drag stories into the sprint, or focus a handle, press Space and use the arrow keys. Left and Right switch lists.</p></div>
  <div class="row row-cols-1 row-cols-sm-2 g-3">
    <div class="col" data-dd-zone><div class="d-flex align-items-center justify-content-between gap-2 mb-2"><h3 class="fw-bold mb-0">Backlog</h3><span class="as-cap badge rounded-pill"><span data-dd-count="sp-backlog">4</span> stories</span></div>
      <ul class="sp-list d-grid gap-2 list-unstyled mb-0" id="sp-backlog" data-dd-list="stories" aria-label="Backlog">
        <li class="sp-row d-flex align-items-center gap-2" data-dd-item data-points="5"><button class="as-handle btn d-grid" type="button" data-dd-handle aria-describedby="sp-hint"><svg class="m-auto" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="sp-title flex-grow-1 fw-semibold" data-dd-name>Password reset flow</span><span class="badge" style="background:#f4f3ff;color:#5925dc">Feature</span><span class="sp-pts rounded-circle d-grid align-content-center text-center fw-bolder" role="img" aria-label="5 points">5</span></li>
        <li class="sp-row d-flex align-items-center gap-2" data-dd-item data-points="3"><button class="as-handle btn d-grid" type="button" data-dd-handle aria-describedby="sp-hint"><svg class="m-auto" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="sp-title flex-grow-1 fw-semibold" data-dd-name>CSV export for invoices</span><span class="badge" style="background:#f4f3ff;color:#5925dc">Feature</span><span class="sp-pts rounded-circle d-grid align-content-center text-center fw-bolder" role="img" aria-label="3 points">3</span></li>
        <li class="sp-row d-flex align-items-center gap-2" data-dd-item data-points="8"><button class="as-handle btn d-grid" type="button" data-dd-handle aria-describedby="sp-hint"><svg class="m-auto" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="sp-title flex-grow-1 fw-semibold" data-dd-name>Onboarding checklist</span><span class="badge" style="background:#f4f3ff;color:#5925dc">Feature</span><span class="sp-pts rounded-circle d-grid align-content-center text-center fw-bolder" role="img" aria-label="8 points">8</span></li>
        <li class="sp-row d-flex align-items-center gap-2" data-dd-item data-points="2"><button class="as-handle btn d-grid" type="button" data-dd-handle aria-describedby="sp-hint"><svg class="m-auto" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="sp-title flex-grow-1 fw-semibold" data-dd-name>Fix timezone bug in reports</span><span class="badge" style="background:#fef3f2;color:#b42318">Bug</span><span class="sp-pts rounded-circle d-grid align-content-center text-center fw-bolder" role="img" aria-label="2 points">2</span></li>
      </ul></div>
    <div class="col" data-dd-zone><div class="d-flex align-items-center justify-content-between gap-2 mb-2"><h3 class="fw-bold mb-0">Sprint 15</h3><span class="as-cap badge rounded-pill"><span data-dd-count="sp-sprint">3</span> stories</span></div>
      <div class="progress" aria-hidden="true"><div class="progress-bar" data-sp-bar style="width:65%"></div></div>
      <p class="sp-sum d-flex justify-content-between gap-2 text-body-secondary mb-2"><span><strong class="text-body" data-sp-points>13</strong> of 20 points</span><span data-sp-note>7 points free</span></p>
      <ul class="sp-list d-grid gap-2 list-unstyled mb-0" id="sp-sprint" data-dd-list="stories" data-capacity="20" aria-label="Sprint 15">
        <li class="sp-row d-flex align-items-center gap-2" data-dd-item data-points="3"><button class="as-handle btn d-grid" type="button" data-dd-handle aria-describedby="sp-hint"><svg class="m-auto" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="sp-title flex-grow-1 fw-semibold" data-dd-name>Dark mode for settings</span><span class="badge" style="background:#f4f3ff;color:#5925dc">Feature</span><span class="sp-pts rounded-circle d-grid align-content-center text-center fw-bolder" role="img" aria-label="3 points">3</span></li>
        <li class="sp-row d-flex align-items-center gap-2" data-dd-item data-points="5"><button class="as-handle btn d-grid" type="button" data-dd-handle aria-describedby="sp-hint"><svg class="m-auto" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="sp-title flex-grow-1 fw-semibold" data-dd-name>Audit log page</span><span class="badge" style="background:#f4f3ff;color:#5925dc">Feature</span><span class="sp-pts rounded-circle d-grid align-content-center text-center fw-bolder" role="img" aria-label="5 points">5</span></li>
        <li class="sp-row d-flex align-items-center gap-2" data-dd-item data-points="5"><button class="as-handle btn d-grid" type="button" data-dd-handle aria-describedby="sp-hint"><svg class="m-auto" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="sp-title flex-grow-1 fw-semibold" data-dd-name>Two-factor sign-in</span><span class="badge" style="background:#ecfdf3;color:#067647">Security</span><span class="sp-pts rounded-circle d-grid align-content-center text-center fw-bolder" role="img" aria-label="5 points">5</span></li>
      </ul></div>
  </div>
  </div>
</section></div>
CSS
/* Drag into targets for Bootstrap 5: panels are .card, team boxes sit in a .row grid, counts are .badge,
   the capacity bar is a .progress; these rules add the drop-target states the script sets. */
.as{--bs-primary:#6d28d9;--bs-primary-rgb:109,40,217;--bs-secondary-color:#5d6679;--as-tint:#f4efff;--as-danger:#b42318;color:#101828}
.as .as-panel{--bs-card-border-color:#e4e7ec;--bs-card-border-radius:16px;padding:16px;box-shadow:0 1px 2px rgba(16,24,40,.05),0 10px 28px -16px rgba(16,24,40,.18)}
.as-head h2{font-size:1rem;letter-spacing:-.01em}
.as-head p{font-size:.82rem}
.as h3{font-size:.86rem}
.as .badge.as-cap{font-size:.75rem;padding:3px 8px;background:#f2f4f7;color:#344054;font-variant-numeric:tabular-nums}
.as-team:has(> [data-full]) .as-cap{background:#fef3f2!important;color:var(--as-danger)!important}
.as-team{--bs-card-bg:#f8f9fb;--bs-card-border-color:#e4e7ec;--bs-card-border-radius:14px;padding:12px}
.as-drop,.sp-list{min-height:52px;padding:4px;border-radius:12px;outline:2px dashed transparent;transition:background-color .15s,outline-color .15s}
.as-drop{align-content:flex-start}
.sp-list{min-height:120px;align-content:start}
.as-drop[data-empty]::after,.sp-list[data-empty]::after{content:"Drop people here";flex:1;display:grid;place-items:center;min-height:44px;border:2px dashed #b3bbc8;border-radius:10px;font-size:.8rem;color:#5d6679}
.sp-list[data-empty]::after{content:"Drop stories here";min-height:100px}
.as[data-dd-active] .as-drop,.as[data-dd-active] .sp-list{outline-color:#c3c9d4}
.as .as-drop[data-over],.as .sp-list[data-over]{outline-color:#6d28d9;background:var(--as-tint)}
.as .as-drop[data-over][data-full]:not(:has([data-dragging],[data-grabbed])){outline-color:var(--as-danger);background:#fef3f2}
.as-chip{padding:4px 12px 4px 4px;background:#fff;border:1px solid #d0d5dd;cursor:grab;user-select:none}
.as-handle{--bs-btn-padding-x:0;--bs-btn-padding-y:0;--bs-btn-color:#667085;--bs-btn-hover-bg:#f2f4f7;--bs-btn-hover-color:#101828;--bs-btn-active-bg:#f2f4f7;
  --bs-btn-border-width:0;--bs-btn-border-radius:999px;width:26px;height:30px;cursor:grab;touch-action:none}
.sp-row .as-handle{--bs-btn-border-radius:7px}
.as-handle:focus-visible{outline:2px solid #6d28d9;outline-offset:1px;box-shadow:none}
.as-av{width:28px;height:28px;font-size:.66rem;font-weight:800}
.as-who{line-height:1.2}
.as-who b{font-size:.84rem;font-weight:600}
.as-who small{font-size:.72rem}
.sp-row{padding:8px 10px 8px 6px;background:#fff;border:1px solid #e4e7ec;border-radius:10px;cursor:grab;user-select:none}
.sp-title{font-size:.86rem}
.sp-row .badge{font-size:.68rem;padding:4px 7px;border-radius:6px}
.sp-pts{min-width:26px;height:26px;font-size:.75rem;background:#f2f4f7;color:#344054}
.as .progress{--bs-progress-height:8px;--bs-progress-bg:#ece6fb;--bs-progress-bar-bg:#6d28d9;--bs-progress-bar-transition:width .25s;margin:2px 0 6px}
.sp-sum{font-size:.78rem}
.as[data-over-capacity] .progress-bar{background:#b54708}
.as[data-over-capacity] [data-sp-note]{color:#b54708;font-weight:700}
.as-chip[data-dragging],.sp-row[data-dragging]{background:var(--as-tint);border:2px dashed #b9a2f5}
.as-chip[data-dragging]>*,.sp-row[data-dragging]>*{visibility:hidden}
.as-chip[data-dd-ghost],.sp-row[data-dd-ghost]{box-shadow:0 16px 32px -10px rgba(16,24,40,.4);rotate:-2deg}
.as-chip[data-grabbed],.sp-row[data-grabbed]{border-color:#6d28d9;box-shadow:0 0 0 2px #6d28d9,0 10px 20px -10px rgba(109,40,217,.6)}
@media (prefers-reduced-motion:reduce){.as-drop,.sp-list{transition:none}.as .progress{--bs-progress-bar-transition:none}.as-chip[data-dd-ghost],.sp-row[data-dd-ghost]{rotate:none}}
JavaScript
// Sprint planner: total the story points in the sprint list and compare them with its capacity.
document.addEventListener('dd:change', e => {
  const root = e.target, list = root.querySelector('[data-capacity]'); if (!list) return;
  const pts = [...list.querySelectorAll('[data-dd-item]')].reduce((s, li) => s + +li.dataset.points, 0), cap = +list.dataset.capacity;
  root.querySelector('[data-sp-points]').textContent = pts;
  root.querySelector('[data-sp-bar]').style.width = Math.min(100, pts / cap * 100) + '%';
  root.querySelector('[data-sp-note]').textContent = pts > cap ? 'Over capacity by ' + (pts - cap) + (pts - cap === 1 ? ' point' : ' points') : (cap - pts) + ' points free';
  root.toggleAttribute('data-over-capacity', pts > cap);
});
// Drag and drop with pointer events (mouse, touch and pen) plus a keyboard mode on every handle:
// Space or Enter picks an item up, the arrow keys move it, Space or Enter drops it, Escape puts it back.
// Every move is announced in the component's aria-live region. Markup contract: [data-dd] root,
// [data-dd-list="group"] lists, [data-dd-item] items with a [data-dd-name] and a [data-dd-handle] button.
// data-dd="grid" or "wrap" compares the pointer horizontally; in a grid, Up and Down jump a whole row.
document.querySelectorAll('[data-dd]').forEach(root => {
  const grid = root.dataset.dd === 'grid', horiz = grid || root.dataset.dd === 'wrap', live = root.querySelector('[data-dd-live]');
  const items = l => [...l.children].filter(c => c.hasAttribute('data-dd-item'));
  const lists = l => [...root.querySelectorAll(`[data-dd-list="${l.dataset.ddList}"]`)];
  const name = el => el.querySelector('[data-dd-name]').textContent.trim();
  const full = l => !!l.dataset.ddMax && items(l).length >= +l.dataset.ddMax;
  const pos = el => { const l = el.parentElement, all = items(l);
    return (lists(l).length > 1 ? l.getAttribute('aria-label') + ', ' : '') + 'position ' + (all.indexOf(el) + 1) + ' of ' + all.length; };
  const say = msg => { live.textContent = ''; setTimeout(() => { live.textContent = msg; }, 60); };
  const clearOver = keep => root.querySelectorAll('[data-over]').forEach(l => l !== keep && l.removeAttribute('data-over'));
  const marks = () => root.querySelectorAll('[data-dd-list]').forEach(l => { const n = items(l).length, c = root.querySelector(`[data-dd-count="${l.id}"]`);
    l.toggleAttribute('data-empty', !n); l.toggleAttribute('data-full', full(l)); if (c) c.textContent = n; });
  const changed = () => {
    marks();
    root.querySelectorAll('[data-dd-item]').forEach(it => it.querySelector('[data-dd-handle]').setAttribute('aria-label', 'Move ' + name(it)));
    root.dispatchEvent(new Event('dd:change', { bubbles: true }));
  };
  let drag, ghost, home, kb, kbh, sx, sy, ox, oy, pid, dragged, moving;
  const putBack = el => home[0].insertBefore(el, home[1] && home[1].parentNode === home[0] ? home[1] : null);

  root.addEventListener('pointerdown', e => {
    const item = e.target.closest('[data-dd-item]'), handle = e.target.closest('[data-dd-handle]');
    if (!item || e.button > 0 || kb) return;
    if (!handle && (e.pointerType !== 'mouse' || 'ddHandleOnly' in root.dataset || e.target.closest('a,button,input,select,textarea,label'))) return;
    e.preventDefault(); drag = item; pid = e.pointerId; sx = e.clientX; sy = e.clientY; ghost = null;
  });
  root.addEventListener('pointermove', e => {
    if (!drag || e.pointerId !== pid) return;
    if (!ghost) { if (Math.hypot(e.clientX - sx, e.clientY - sy) < 6) return; start(); root.setPointerCapture(pid); }
    ghost.style.translate = `${e.clientX - ox}px ${e.clientY - oy}px`;
    if (e.clientY < 40) scrollBy(0, -12); else if (e.clientY > innerHeight - 40) scrollBy(0, 12);
    const hit = document.elementFromPoint(e.clientX, e.clientY), sel = `[data-dd-list="${home[0].dataset.ddList}"]`;
    const list = hit && (hit.closest(sel) || hit.closest('[data-dd-zone]')?.querySelector(sel)); // a [data-dd-zone] wrapper widens the target
    clearOver(list);
    if (!list || !root.contains(list) || drag.contains(list)) return;
    list.setAttribute('data-over', '');
    if (list !== drag.parentElement && full(list)) return;
    const over = items(list).find(c => c !== drag && c.contains(hit));
    if (over) { const r = over.getBoundingClientRect();
      const ref = (horiz ? e.clientX > r.left + r.width / 2 : e.clientY > r.top + r.height / 2) ? over.nextSibling : over;
      if (ref !== drag && ref !== drag.nextSibling) { list.insertBefore(drag, ref); marks(); }
    } else if (list !== drag.parentElement && !drag.contains(hit)) { list.append(drag); marks(); }
  });
  const end = e => {
    if (!drag || e.pointerId !== pid) return;
    if (ghost) {
      ghost.remove(); drag.removeAttribute('data-dragging'); root.removeAttribute('data-dd-active'); document.body.style.cursor = ''; clearOver();
      if (e.type === 'pointercancel') putBack(drag); else say(name(drag) + ' moved to ' + pos(drag) + '.');
      changed(); dragged = true; setTimeout(() => { dragged = false; });
    }
    drag = null;
  };
  root.addEventListener('pointerup', end); root.addEventListener('pointercancel', end);
  function start() {
    const r = drag.getBoundingClientRect(); ox = sx - r.left; oy = sy - r.top; home = [drag.parentElement, drag.nextSibling];
    ghost = drag.cloneNode(true); ghost.removeAttribute('data-dd-item'); ghost.setAttribute('data-dd-ghost', ''); ghost.setAttribute('aria-hidden', 'true'); ghost.inert = true;
    ghost.querySelectorAll('[id]').forEach(n => n.removeAttribute('id'));
    Object.assign(ghost.style, { position: 'fixed', left: 0, top: 0, width: r.width + 'px', height: r.height + 'px', margin: 0, zIndex: 999, pointerEvents: 'none' });
    drag.after(ghost); drag.setAttribute('data-dragging', ''); root.setAttribute('data-dd-active', ''); document.body.style.cursor = 'grabbing';
  }

  // Keyboard and screen reader mode: the handle is a toggle button (aria-pressed) that picks the item up.
  const finish = msg => { kb.removeAttribute('data-grabbed'); kbh.setAttribute('aria-pressed', 'false'); root.removeAttribute('data-dd-active'); clearOver(); kb = null; changed(); say(msg); };
  const move = fn => { moving = true; fn(); kbh.focus(); moving = false; kbh.scrollIntoView({ block: 'nearest' }); clearOver(kb.parentElement); kb.parentElement.setAttribute('data-over', ''); changed(); say(name(kb) + ' moved to ' + pos(kb) + '.'); };
  root.addEventListener('click', e => {
    const h = e.target.closest('[data-dd-handle]'); if (!h || dragged) return;
    const item = h.closest('[data-dd-item]');
    if (kb === item) return finish(name(item) + ' dropped, ' + pos(item) + '.');
    if (kb) finish(name(kb) + ' dropped, ' + pos(kb) + '.');
    kb = item; kbh = h; home = [item.parentElement, item.nextSibling];
    item.setAttribute('data-grabbed', ''); h.setAttribute('aria-pressed', 'true'); root.setAttribute('data-dd-active', ''); item.parentElement.setAttribute('data-over', ''); h.focus();
    say(name(item) + ' picked up, ' + pos(item) + '. Use the arrow keys to move it, Space to drop it, Escape to cancel.');
  });
  root.addEventListener('keydown', e => {
    if (!kb || e.target !== kbh) return;
    if (e.key === 'Escape') { e.preventDefault(); moving = true; putBack(kb); kbh.focus(); moving = false; return finish('Move cancelled. ' + name(kb) + ' returned to ' + pos(kb) + '.'); }
    const d = { ArrowUp: -1, ArrowLeft: -1, ArrowDown: 1, ArrowRight: 1 }[e.key]; if (!d) return;
    e.preventDefault();
    const l = kb.parentElement, all = items(l), i = all.indexOf(kb), ls = lists(l), side = e.key === 'ArrowLeft' || e.key === 'ArrowRight';
    if (side && ls.length > 1) {
      const t = ls[ls.indexOf(l) + d];
      if (!t) return say(name(kb) + ' is already in ' + l.getAttribute('aria-label') + ', the ' + (d < 0 ? 'first' : 'last') + ' list.');
      if (full(t)) return say(t.getAttribute('aria-label') + ' is full.');
      return move(() => t.insertBefore(kb, items(t)[i] || null));
    }
    const step = grid && !side ? d * all.filter(x => x.offsetTop === all[0].offsetTop).length : d, j = Math.max(0, Math.min(all.length - 1, i + step));
    if (j === i) return say(name(kb) + ' is already ' + (d < 0 ? 'first' : 'last') + '.');
    move(() => l.insertBefore(kb, j > i ? all[j].nextSibling : all[j]));
  });
  root.addEventListener('focusout', e => { if (kb && !moving && e.target === kbh) finish(name(kb) + ' dropped, ' + pos(kb) + '.'); });
  changed();
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-as-muted uppercase">Assign people to teams</span>
<section class="group/as text-as-ink" data-dd="wrap" aria-labelledby="as-title">
  <div class="rounded-2xl border border-as-line bg-white p-4 shadow-[0_1px_2px_rgba(16,24,40,.05),0_10px_28px_-16px_rgba(16,24,40,.18)]" data-dd-zone>
    <div class="mb-3 flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1.5"><h2 class="font-bold tracking-[-.01em]" id="as-title">Autumn hackathon teams</h2><p class="text-[.82rem] text-as-muted" id="as-hint">Drag a person onto a team, or focus their handle, press Space, then use Left and Right to change team.</p></div>
    <div class="mb-2.5 flex items-center justify-between gap-2"><h3 class="text-[.86rem] font-bold">Unassigned</h3><span class="rounded-full bg-[#f2f4f7] px-2 py-px text-[.75rem] font-bold text-[#344054] tabular-nums"><span data-dd-count="as-pool">3</span> left</span></div>
    <ul class="flex min-h-[52px] flex-wrap content-start gap-2 rounded-xl p-1 outline-2 outline-dashed outline-transparent transition-[background-color,outline-color] motion-reduce:transition-none group-data-[dd-active]/as:outline-[#c3c9d4] data-over:bg-as-tint data-over:outline-as-accent! [&[data-over][data-full]:not(:has([data-dragging],[data-grabbed]))]:bg-[#fef3f2] [&[data-over][data-full]:not(:has([data-dragging],[data-grabbed]))]:outline-as-danger! data-empty:after:grid data-empty:after:place-items-center data-empty:after:rounded-[10px] data-empty:after:border-2 data-empty:after:border-dashed data-empty:after:border-[#b3bbc8] data-empty:after:text-[.8rem] data-empty:after:text-as-muted data-empty:after:min-h-11 data-empty:after:flex-1 data-empty:after:content-['Drop_people_here'] *:data-dragging:border-2 *:data-dragging:border-dashed *:data-dragging:border-[#b9a2f5] *:data-dragging:bg-as-tint [&>[data-dragging]>*]:invisible *:data-dd-ghost:-rotate-2 *:data-dd-ghost:shadow-[0_16px_32px_-10px_rgba(16,24,40,.4)] motion-reduce:*:data-dd-ghost:rotate-0 *:data-grabbed:border-as-accent *:data-grabbed:shadow-[0_0_0_2px_#6d28d9,0_10px_20px_-10px_rgba(109,40,217,.6)]" id="as-pool" data-dd-list="people" aria-label="Unassigned">
      <li class="flex cursor-grab items-center gap-2 rounded-full border border-[#d0d5dd] bg-white py-1 pr-3 pl-1 select-none" data-dd-item><button class="grid h-[30px] w-[26px] cursor-grab touch-none place-items-center rounded-full text-[#667085] hover:bg-[#f2f4f7] hover:text-as-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-as-accent" type="button" data-dd-handle aria-describedby="as-hint"><svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="grid size-7 place-items-center rounded-full bg-[#fef6ee] text-[.66rem] font-extrabold text-[#b93815]" aria-hidden="true">CM</span><span class="grid leading-[1.2]"><b class="text-[.84rem] font-semibold" data-dd-name>Chloé Martin</b><small class="text-[.72rem] text-as-muted">UX research</small></span></li>
      <li class="flex cursor-grab items-center gap-2 rounded-full border border-[#d0d5dd] bg-white py-1 pr-3 pl-1 select-none" data-dd-item><button class="grid h-[30px] w-[26px] cursor-grab touch-none place-items-center rounded-full text-[#667085] hover:bg-[#f2f4f7] hover:text-as-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-as-accent" type="button" data-dd-handle aria-describedby="as-hint"><svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="grid size-7 place-items-center rounded-full bg-[#f0f9ff] text-[.66rem] font-extrabold text-[#026aa2]" aria-hidden="true">FW</span><span class="grid leading-[1.2]"><b class="text-[.84rem] font-semibold" data-dd-name>Felix Wu</b><small class="text-[.72rem] text-as-muted">Data analyst</small></span></li>
      <li class="flex cursor-grab items-center gap-2 rounded-full border border-[#d0d5dd] bg-white py-1 pr-3 pl-1 select-none" data-dd-item><button class="grid h-[30px] w-[26px] cursor-grab touch-none place-items-center rounded-full text-[#667085] hover:bg-[#f2f4f7] hover:text-as-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-as-accent" type="button" data-dd-handle aria-describedby="as-hint"><svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="grid size-7 place-items-center rounded-full bg-[#f2f4f7] text-[.66rem] font-extrabold text-[#344054]" aria-hidden="true">HS</span><span class="grid leading-[1.2]"><b class="text-[.84rem] font-semibold" data-dd-name>Hugo Silva</b><small class="text-[.72rem] text-as-muted">Content design</small></span></li>
    </ul>
  </div>
  <div class="mt-3 grid grid-cols-[repeat(auto-fit,minmax(200px,1fr))] gap-3">
    <div data-dd-zone class="group/team rounded-[14px] border border-as-line bg-[#f8f9fb] p-3"><div class="mb-2.5 flex items-center justify-between gap-2"><h3 class="text-[.86rem] font-bold">Design</h3><span class="rounded-full bg-[#f2f4f7] px-2 py-px text-[.75rem] font-bold text-[#344054] tabular-nums group-has-[>[data-full]]/team:bg-[#fef3f2] group-has-[>[data-full]]/team:text-as-danger"><span data-dd-count="as-design">2</span> / 3<span class="sr-only"> people</span></span></div>
      <ul class="flex min-h-[52px] flex-wrap content-start gap-2 rounded-xl p-1 outline-2 outline-dashed outline-transparent transition-[background-color,outline-color] motion-reduce:transition-none group-data-[dd-active]/as:outline-[#c3c9d4] data-over:bg-as-tint data-over:outline-as-accent! [&[data-over][data-full]:not(:has([data-dragging],[data-grabbed]))]:bg-[#fef3f2] [&[data-over][data-full]:not(:has([data-dragging],[data-grabbed]))]:outline-as-danger! data-empty:after:grid data-empty:after:place-items-center data-empty:after:rounded-[10px] data-empty:after:border-2 data-empty:after:border-dashed data-empty:after:border-[#b3bbc8] data-empty:after:text-[.8rem] data-empty:after:text-as-muted data-empty:after:min-h-11 data-empty:after:flex-1 data-empty:after:content-['Drop_people_here'] *:data-dragging:border-2 *:data-dragging:border-dashed *:data-dragging:border-[#b9a2f5] *:data-dragging:bg-as-tint [&>[data-dragging]>*]:invisible *:data-dd-ghost:-rotate-2 *:data-dd-ghost:shadow-[0_16px_32px_-10px_rgba(16,24,40,.4)] motion-reduce:*:data-dd-ghost:rotate-0 *:data-grabbed:border-as-accent *:data-grabbed:shadow-[0_0_0_2px_#6d28d9,0_10px_20px_-10px_rgba(109,40,217,.6)]" id="as-design" data-dd-list="people" data-dd-max="3" aria-label="Design">
        <li class="flex cursor-grab items-center gap-2 rounded-full border border-[#d0d5dd] bg-white py-1 pr-3 pl-1 select-none" data-dd-item><button class="grid h-[30px] w-[26px] cursor-grab touch-none place-items-center rounded-full text-[#667085] hover:bg-[#f2f4f7] hover:text-as-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-as-accent" type="button" data-dd-handle aria-describedby="as-hint"><svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="grid size-7 place-items-center rounded-full bg-[#fdf2fa] text-[.66rem] font-extrabold text-[#9e165f]" aria-hidden="true">AR</span><span class="grid leading-[1.2]"><b class="text-[.84rem] font-semibold" data-dd-name>Ana Ruiz</b><small class="text-[.72rem] text-as-muted">Illustrator</small></span></li>
        <li class="flex cursor-grab items-center gap-2 rounded-full border border-[#d0d5dd] bg-white py-1 pr-3 pl-1 select-none" data-dd-item><button class="grid h-[30px] w-[26px] cursor-grab touch-none place-items-center rounded-full text-[#667085] hover:bg-[#f2f4f7] hover:text-as-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-as-accent" type="button" data-dd-handle aria-describedby="as-hint"><svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="grid size-7 place-items-center rounded-full bg-[#f4f3ff] text-[.66rem] font-extrabold text-[#5925dc]" aria-hidden="true">EN</span><span class="grid leading-[1.2]"><b class="text-[.84rem] font-semibold" data-dd-name>Emma Novak</b><small class="text-[.72rem] text-as-muted">Product design</small></span></li>
      </ul></div>
    <div data-dd-zone class="group/team rounded-[14px] border border-as-line bg-[#f8f9fb] p-3"><div class="mb-2.5 flex items-center justify-between gap-2"><h3 class="text-[.86rem] font-bold">Engineering</h3><span class="rounded-full bg-[#f2f4f7] px-2 py-px text-[.75rem] font-bold text-[#344054] tabular-nums group-has-[>[data-full]]/team:bg-[#fef3f2] group-has-[>[data-full]]/team:text-as-danger"><span data-dd-count="as-eng">3</span> / 4<span class="sr-only"> people</span></span></div>
      <ul class="flex min-h-[52px] flex-wrap content-start gap-2 rounded-xl p-1 outline-2 outline-dashed outline-transparent transition-[background-color,outline-color] motion-reduce:transition-none group-data-[dd-active]/as:outline-[#c3c9d4] data-over:bg-as-tint data-over:outline-as-accent! [&[data-over][data-full]:not(:has([data-dragging],[data-grabbed]))]:bg-[#fef3f2] [&[data-over][data-full]:not(:has([data-dragging],[data-grabbed]))]:outline-as-danger! data-empty:after:grid data-empty:after:place-items-center data-empty:after:rounded-[10px] data-empty:after:border-2 data-empty:after:border-dashed data-empty:after:border-[#b3bbc8] data-empty:after:text-[.8rem] data-empty:after:text-as-muted data-empty:after:min-h-11 data-empty:after:flex-1 data-empty:after:content-['Drop_people_here'] *:data-dragging:border-2 *:data-dragging:border-dashed *:data-dragging:border-[#b9a2f5] *:data-dragging:bg-as-tint [&>[data-dragging]>*]:invisible *:data-dd-ghost:-rotate-2 *:data-dd-ghost:shadow-[0_16px_32px_-10px_rgba(16,24,40,.4)] motion-reduce:*:data-dd-ghost:rotate-0 *:data-grabbed:border-as-accent *:data-grabbed:shadow-[0_0_0_2px_#6d28d9,0_10px_20px_-10px_rgba(109,40,217,.6)]" id="as-eng" data-dd-list="people" data-dd-max="4" aria-label="Engineering">
        <li class="flex cursor-grab items-center gap-2 rounded-full border border-[#d0d5dd] bg-white py-1 pr-3 pl-1 select-none" data-dd-item><button class="grid h-[30px] w-[26px] cursor-grab touch-none place-items-center rounded-full text-[#667085] hover:bg-[#f2f4f7] hover:text-as-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-as-accent" type="button" data-dd-handle aria-describedby="as-hint"><svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="grid size-7 place-items-center rounded-full bg-[#eef4ff] text-[.66rem] font-extrabold text-[#3538cd]" aria-hidden="true">BC</span><span class="grid leading-[1.2]"><b class="text-[.84rem] font-semibold" data-dd-name>Ben Carter</b><small class="text-[.72rem] text-as-muted">Front-end</small></span></li>
        <li class="flex cursor-grab items-center gap-2 rounded-full border border-[#d0d5dd] bg-white py-1 pr-3 pl-1 select-none" data-dd-item><button class="grid h-[30px] w-[26px] cursor-grab touch-none place-items-center rounded-full text-[#667085] hover:bg-[#f2f4f7] hover:text-as-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-as-accent" type="button" data-dd-handle aria-describedby="as-hint"><svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="grid size-7 place-items-center rounded-full bg-[#ecfdf3] text-[.66rem] font-extrabold text-[#067647]" aria-hidden="true">DP</span><span class="grid leading-[1.2]"><b class="text-[.84rem] font-semibold" data-dd-name>Dev Patel</b><small class="text-[.72rem] text-as-muted">Back-end</small></span></li>
        <li class="flex cursor-grab items-center gap-2 rounded-full border border-[#d0d5dd] bg-white py-1 pr-3 pl-1 select-none" data-dd-item><button class="grid h-[30px] w-[26px] cursor-grab touch-none place-items-center rounded-full text-[#667085] hover:bg-[#f2f4f7] hover:text-as-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-as-accent" type="button" data-dd-handle aria-describedby="as-hint"><svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="grid size-7 place-items-center rounded-full bg-[#fefbe8] text-[.66rem] font-extrabold text-[#a15c07]" aria-hidden="true">GL</span><span class="grid leading-[1.2]"><b class="text-[.84rem] font-semibold" data-dd-name>Grace Liu</b><small class="text-[.72rem] text-as-muted">QA</small></span></li>
      </ul></div>
    <div data-dd-zone class="group/team rounded-[14px] border border-as-line bg-[#f8f9fb] p-3"><div class="mb-2.5 flex items-center justify-between gap-2"><h3 class="text-[.86rem] font-bold">Research</h3><span class="rounded-full bg-[#f2f4f7] px-2 py-px text-[.75rem] font-bold text-[#344054] tabular-nums group-has-[>[data-full]]/team:bg-[#fef3f2] group-has-[>[data-full]]/team:text-as-danger"><span data-dd-count="as-research">0</span> / 2<span class="sr-only"> people</span></span></div>
      <ul class="flex min-h-[52px] flex-wrap content-start gap-2 rounded-xl p-1 outline-2 outline-dashed outline-transparent transition-[background-color,outline-color] motion-reduce:transition-none group-data-[dd-active]/as:outline-[#c3c9d4] data-over:bg-as-tint data-over:outline-as-accent! [&[data-over][data-full]:not(:has([data-dragging],[data-grabbed]))]:bg-[#fef3f2] [&[data-over][data-full]:not(:has([data-dragging],[data-grabbed]))]:outline-as-danger! data-empty:after:grid data-empty:after:place-items-center data-empty:after:rounded-[10px] data-empty:after:border-2 data-empty:after:border-dashed data-empty:after:border-[#b3bbc8] data-empty:after:text-[.8rem] data-empty:after:text-as-muted data-empty:after:min-h-11 data-empty:after:flex-1 data-empty:after:content-['Drop_people_here'] *:data-dragging:border-2 *:data-dragging:border-dashed *:data-dragging:border-[#b9a2f5] *:data-dragging:bg-as-tint [&>[data-dragging]>*]:invisible *:data-dd-ghost:-rotate-2 *:data-dd-ghost:shadow-[0_16px_32px_-10px_rgba(16,24,40,.4)] motion-reduce:*:data-dd-ghost:rotate-0 *:data-grabbed:border-as-accent *:data-grabbed:shadow-[0_0_0_2px_#6d28d9,0_10px_20px_-10px_rgba(109,40,217,.6)]" id="as-research" data-dd-list="people" data-dd-max="2" aria-label="Research">
      </ul></div>
  </div>
  <p class="sr-only" aria-live="assertive" aria-atomic="true" data-dd-live></p>
</section></div>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-as-muted uppercase">Plan a sprint against its capacity</span>
<section class="group/as text-as-ink rounded-2xl border border-as-line bg-white p-4 shadow-[0_1px_2px_rgba(16,24,40,.05),0_10px_28px_-16px_rgba(16,24,40,.18)]" data-dd aria-labelledby="sp-title">
  <div class="mb-3 flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1.5"><h2 class="font-bold tracking-[-.01em]" id="sp-title">Sprint 15 · Oct 21 to Nov 1</h2><p class="text-[.82rem] text-as-muted" id="sp-hint">Drag stories into the sprint, or focus a handle, press Space and use the arrow keys. Left and Right switch lists.</p></div>
  <div class="grid grid-cols-2 gap-3 max-[560px]:grid-cols-1">
    <div data-dd-zone><div class="mb-2.5 flex items-center justify-between gap-2"><h3 class="text-[.86rem] font-bold">Backlog</h3><span class="rounded-full bg-[#f2f4f7] px-2 py-px text-[.75rem] font-bold text-[#344054] tabular-nums"><span data-dd-count="sp-backlog">4</span> stories</span></div>
      <ul class="grid min-h-[120px] content-start gap-1.5 rounded-xl p-1 outline-2 outline-dashed outline-transparent transition-[background-color,outline-color] motion-reduce:transition-none group-data-[dd-active]/as:outline-[#c3c9d4] data-over:bg-as-tint data-over:outline-as-accent! [&[data-over][data-full]:not(:has([data-dragging],[data-grabbed]))]:bg-[#fef3f2] [&[data-over][data-full]:not(:has([data-dragging],[data-grabbed]))]:outline-as-danger! data-empty:after:grid data-empty:after:place-items-center data-empty:after:rounded-[10px] data-empty:after:border-2 data-empty:after:border-dashed data-empty:after:border-[#b3bbc8] data-empty:after:text-[.8rem] data-empty:after:text-as-muted data-empty:after:min-h-[100px] data-empty:after:content-['Drop_stories_here'] *:data-dragging:border-2 *:data-dragging:border-dashed *:data-dragging:border-[#b9a2f5] *:data-dragging:bg-as-tint [&>[data-dragging]>*]:invisible *:data-dd-ghost:-rotate-2 *:data-dd-ghost:shadow-[0_16px_32px_-10px_rgba(16,24,40,.4)] motion-reduce:*:data-dd-ghost:rotate-0 *:data-grabbed:border-as-accent *:data-grabbed:shadow-[0_0_0_2px_#6d28d9,0_10px_20px_-10px_rgba(109,40,217,.6)]" id="sp-backlog" data-dd-list="stories" aria-label="Backlog">
        <li class="flex cursor-grab items-center gap-2 rounded-[10px] border border-as-line bg-white py-2 pr-2.5 pl-1.5 select-none" data-dd-item data-points="5"><button class="grid h-[30px] w-[26px] cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-as-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-as-accent" type="button" data-dd-handle aria-describedby="sp-hint"><svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="min-w-0 flex-1 text-[.86rem] font-semibold" data-dd-name>Password reset flow</span><span class="rounded-md bg-[#f4f3ff] px-[7px] py-0.5 text-[.68rem] font-bold text-[#5925dc]">Feature</span><span class="grid h-[26px] min-w-[26px] place-items-center rounded-full bg-[#f2f4f7] text-[.75rem] font-extrabold text-[#344054]" role="img" aria-label="5 points">5</span></li>
        <li class="flex cursor-grab items-center gap-2 rounded-[10px] border border-as-line bg-white py-2 pr-2.5 pl-1.5 select-none" data-dd-item data-points="3"><button class="grid h-[30px] w-[26px] cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-as-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-as-accent" type="button" data-dd-handle aria-describedby="sp-hint"><svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="min-w-0 flex-1 text-[.86rem] font-semibold" data-dd-name>CSV export for invoices</span><span class="rounded-md bg-[#f4f3ff] px-[7px] py-0.5 text-[.68rem] font-bold text-[#5925dc]">Feature</span><span class="grid h-[26px] min-w-[26px] place-items-center rounded-full bg-[#f2f4f7] text-[.75rem] font-extrabold text-[#344054]" role="img" aria-label="3 points">3</span></li>
        <li class="flex cursor-grab items-center gap-2 rounded-[10px] border border-as-line bg-white py-2 pr-2.5 pl-1.5 select-none" data-dd-item data-points="8"><button class="grid h-[30px] w-[26px] cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-as-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-as-accent" type="button" data-dd-handle aria-describedby="sp-hint"><svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="min-w-0 flex-1 text-[.86rem] font-semibold" data-dd-name>Onboarding checklist</span><span class="rounded-md bg-[#f4f3ff] px-[7px] py-0.5 text-[.68rem] font-bold text-[#5925dc]">Feature</span><span class="grid h-[26px] min-w-[26px] place-items-center rounded-full bg-[#f2f4f7] text-[.75rem] font-extrabold text-[#344054]" role="img" aria-label="8 points">8</span></li>
        <li class="flex cursor-grab items-center gap-2 rounded-[10px] border border-as-line bg-white py-2 pr-2.5 pl-1.5 select-none" data-dd-item data-points="2"><button class="grid h-[30px] w-[26px] cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-as-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-as-accent" type="button" data-dd-handle aria-describedby="sp-hint"><svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="min-w-0 flex-1 text-[.86rem] font-semibold" data-dd-name>Fix timezone bug in reports</span><span class="rounded-md bg-[#fef3f2] px-[7px] py-0.5 text-[.68rem] font-bold text-[#b42318]">Bug</span><span class="grid h-[26px] min-w-[26px] place-items-center rounded-full bg-[#f2f4f7] text-[.75rem] font-extrabold text-[#344054]" role="img" aria-label="2 points">2</span></li>
      </ul></div>
    <div data-dd-zone><div class="mb-2.5 flex items-center justify-between gap-2"><h3 class="text-[.86rem] font-bold">Sprint 15</h3><span class="rounded-full bg-[#f2f4f7] px-2 py-px text-[.75rem] font-bold text-[#344054] tabular-nums"><span data-dd-count="sp-sprint">3</span> stories</span></div>
      <div class="mt-0.5 mb-1.5 h-2 overflow-hidden rounded-full bg-[#ece6fb]" aria-hidden="true"><span class="block h-full rounded-full bg-as-accent transition-[width] duration-250 group-data-[over-capacity]/as:bg-[#b54708] motion-reduce:transition-none" data-sp-bar style="width:65%"></span></div>
      <p class="mb-2 flex justify-between gap-2 text-[.78rem] text-as-muted"><span><strong class="text-as-ink" data-sp-points>13</strong> of 20 points</span><span class="group-data-[over-capacity]/as:font-bold group-data-[over-capacity]/as:text-[#b54708]" data-sp-note>7 points free</span></p>
      <ul class="grid min-h-[120px] content-start gap-1.5 rounded-xl p-1 outline-2 outline-dashed outline-transparent transition-[background-color,outline-color] motion-reduce:transition-none group-data-[dd-active]/as:outline-[#c3c9d4] data-over:bg-as-tint data-over:outline-as-accent! [&[data-over][data-full]:not(:has([data-dragging],[data-grabbed]))]:bg-[#fef3f2] [&[data-over][data-full]:not(:has([data-dragging],[data-grabbed]))]:outline-as-danger! data-empty:after:grid data-empty:after:place-items-center data-empty:after:rounded-[10px] data-empty:after:border-2 data-empty:after:border-dashed data-empty:after:border-[#b3bbc8] data-empty:after:text-[.8rem] data-empty:after:text-as-muted data-empty:after:min-h-[100px] data-empty:after:content-['Drop_stories_here'] *:data-dragging:border-2 *:data-dragging:border-dashed *:data-dragging:border-[#b9a2f5] *:data-dragging:bg-as-tint [&>[data-dragging]>*]:invisible *:data-dd-ghost:-rotate-2 *:data-dd-ghost:shadow-[0_16px_32px_-10px_rgba(16,24,40,.4)] motion-reduce:*:data-dd-ghost:rotate-0 *:data-grabbed:border-as-accent *:data-grabbed:shadow-[0_0_0_2px_#6d28d9,0_10px_20px_-10px_rgba(109,40,217,.6)]" id="sp-sprint" data-dd-list="stories" data-capacity="20" aria-label="Sprint 15">
        <li class="flex cursor-grab items-center gap-2 rounded-[10px] border border-as-line bg-white py-2 pr-2.5 pl-1.5 select-none" data-dd-item data-points="3"><button class="grid h-[30px] w-[26px] cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-as-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-as-accent" type="button" data-dd-handle aria-describedby="sp-hint"><svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="min-w-0 flex-1 text-[.86rem] font-semibold" data-dd-name>Dark mode for settings</span><span class="rounded-md bg-[#f4f3ff] px-[7px] py-0.5 text-[.68rem] font-bold text-[#5925dc]">Feature</span><span class="grid h-[26px] min-w-[26px] place-items-center rounded-full bg-[#f2f4f7] text-[.75rem] font-extrabold text-[#344054]" role="img" aria-label="3 points">3</span></li>
        <li class="flex cursor-grab items-center gap-2 rounded-[10px] border border-as-line bg-white py-2 pr-2.5 pl-1.5 select-none" data-dd-item data-points="5"><button class="grid h-[30px] w-[26px] cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-as-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-as-accent" type="button" data-dd-handle aria-describedby="sp-hint"><svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="min-w-0 flex-1 text-[.86rem] font-semibold" data-dd-name>Audit log page</span><span class="rounded-md bg-[#f4f3ff] px-[7px] py-0.5 text-[.68rem] font-bold text-[#5925dc]">Feature</span><span class="grid h-[26px] min-w-[26px] place-items-center rounded-full bg-[#f2f4f7] text-[.75rem] font-extrabold text-[#344054]" role="img" aria-label="5 points">5</span></li>
        <li class="flex cursor-grab items-center gap-2 rounded-[10px] border border-as-line bg-white py-2 pr-2.5 pl-1.5 select-none" data-dd-item data-points="5"><button class="grid h-[30px] w-[26px] cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-as-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-as-accent" type="button" data-dd-handle aria-describedby="sp-hint"><svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="7" cy="5" r="1.5"/><circle cx="13" cy="5" r="1.5"/><circle cx="7" cy="10" r="1.5"/><circle cx="13" cy="10" r="1.5"/><circle cx="7" cy="15" r="1.5"/><circle cx="13" cy="15" r="1.5"/></svg></button><span class="min-w-0 flex-1 text-[.86rem] font-semibold" data-dd-name>Two-factor sign-in</span><span class="rounded-md bg-[#ecfdf3] px-[7px] py-0.5 text-[.68rem] font-bold text-[#067647]">Security</span><span class="grid h-[26px] min-w-[26px] place-items-center rounded-full bg-[#f2f4f7] text-[.75rem] font-extrabold text-[#344054]" role="img" aria-label="5 points">5</span></li>
      </ul></div>
  </div>
  <p class="sr-only" aria-live="assertive" aria-atomic="true" data-dd-live></p>
</section></div>
CSS
@theme {
  --color-as-accent: #6d28d9;
  --color-as-tint: #f4efff;
  --color-as-line: #e4e7ec;
  --color-as-muted: #5d6679;
  --color-as-ink: #101828;
  --color-as-danger: #b42318;
}
JavaScript
// Sprint planner: total the story points in the sprint list and compare them with its capacity.
document.addEventListener('dd:change', e => {
  const root = e.target, list = root.querySelector('[data-capacity]'); if (!list) return;
  const pts = [...list.querySelectorAll('[data-dd-item]')].reduce((s, li) => s + +li.dataset.points, 0), cap = +list.dataset.capacity;
  root.querySelector('[data-sp-points]').textContent = pts;
  root.querySelector('[data-sp-bar]').style.width = Math.min(100, pts / cap * 100) + '%';
  root.querySelector('[data-sp-note]').textContent = pts > cap ? 'Over capacity by ' + (pts - cap) + (pts - cap === 1 ? ' point' : ' points') : (cap - pts) + ' points free';
  root.toggleAttribute('data-over-capacity', pts > cap);
});
// Drag and drop with pointer events (mouse, touch and pen) plus a keyboard mode on every handle:
// Space or Enter picks an item up, the arrow keys move it, Space or Enter drops it, Escape puts it back.
// Every move is announced in the component's aria-live region. Markup contract: [data-dd] root,
// [data-dd-list="group"] lists, [data-dd-item] items with a [data-dd-name] and a [data-dd-handle] button.
// data-dd="grid" or "wrap" compares the pointer horizontally; in a grid, Up and Down jump a whole row.
document.querySelectorAll('[data-dd]').forEach(root => {
  const grid = root.dataset.dd === 'grid', horiz = grid || root.dataset.dd === 'wrap', live = root.querySelector('[data-dd-live]');
  const items = l => [...l.children].filter(c => c.hasAttribute('data-dd-item'));
  const lists = l => [...root.querySelectorAll(`[data-dd-list="${l.dataset.ddList}"]`)];
  const name = el => el.querySelector('[data-dd-name]').textContent.trim();
  const full = l => !!l.dataset.ddMax && items(l).length >= +l.dataset.ddMax;
  const pos = el => { const l = el.parentElement, all = items(l);
    return (lists(l).length > 1 ? l.getAttribute('aria-label') + ', ' : '') + 'position ' + (all.indexOf(el) + 1) + ' of ' + all.length; };
  const say = msg => { live.textContent = ''; setTimeout(() => { live.textContent = msg; }, 60); };
  const clearOver = keep => root.querySelectorAll('[data-over]').forEach(l => l !== keep && l.removeAttribute('data-over'));
  const marks = () => root.querySelectorAll('[data-dd-list]').forEach(l => { const n = items(l).length, c = root.querySelector(`[data-dd-count="${l.id}"]`);
    l.toggleAttribute('data-empty', !n); l.toggleAttribute('data-full', full(l)); if (c) c.textContent = n; });
  const changed = () => {
    marks();
    root.querySelectorAll('[data-dd-item]').forEach(it => it.querySelector('[data-dd-handle]').setAttribute('aria-label', 'Move ' + name(it)));
    root.dispatchEvent(new Event('dd:change', { bubbles: true }));
  };
  let drag, ghost, home, kb, kbh, sx, sy, ox, oy, pid, dragged, moving;
  const putBack = el => home[0].insertBefore(el, home[1] && home[1].parentNode === home[0] ? home[1] : null);

  root.addEventListener('pointerdown', e => {
    const item = e.target.closest('[data-dd-item]'), handle = e.target.closest('[data-dd-handle]');
    if (!item || e.button > 0 || kb) return;
    if (!handle && (e.pointerType !== 'mouse' || 'ddHandleOnly' in root.dataset || e.target.closest('a,button,input,select,textarea,label'))) return;
    e.preventDefault(); drag = item; pid = e.pointerId; sx = e.clientX; sy = e.clientY; ghost = null;
  });
  root.addEventListener('pointermove', e => {
    if (!drag || e.pointerId !== pid) return;
    if (!ghost) { if (Math.hypot(e.clientX - sx, e.clientY - sy) < 6) return; start(); root.setPointerCapture(pid); }
    ghost.style.translate = `${e.clientX - ox}px ${e.clientY - oy}px`;
    if (e.clientY < 40) scrollBy(0, -12); else if (e.clientY > innerHeight - 40) scrollBy(0, 12);
    const hit = document.elementFromPoint(e.clientX, e.clientY), sel = `[data-dd-list="${home[0].dataset.ddList}"]`;
    const list = hit && (hit.closest(sel) || hit.closest('[data-dd-zone]')?.querySelector(sel)); // a [data-dd-zone] wrapper widens the target
    clearOver(list);
    if (!list || !root.contains(list) || drag.contains(list)) return;
    list.setAttribute('data-over', '');
    if (list !== drag.parentElement && full(list)) return;
    const over = items(list).find(c => c !== drag && c.contains(hit));
    if (over) { const r = over.getBoundingClientRect();
      const ref = (horiz ? e.clientX > r.left + r.width / 2 : e.clientY > r.top + r.height / 2) ? over.nextSibling : over;
      if (ref !== drag && ref !== drag.nextSibling) { list.insertBefore(drag, ref); marks(); }
    } else if (list !== drag.parentElement && !drag.contains(hit)) { list.append(drag); marks(); }
  });
  const end = e => {
    if (!drag || e.pointerId !== pid) return;
    if (ghost) {
      ghost.remove(); drag.removeAttribute('data-dragging'); root.removeAttribute('data-dd-active'); document.body.style.cursor = ''; clearOver();
      if (e.type === 'pointercancel') putBack(drag); else say(name(drag) + ' moved to ' + pos(drag) + '.');
      changed(); dragged = true; setTimeout(() => { dragged = false; });
    }
    drag = null;
  };
  root.addEventListener('pointerup', end); root.addEventListener('pointercancel', end);
  function start() {
    const r = drag.getBoundingClientRect(); ox = sx - r.left; oy = sy - r.top; home = [drag.parentElement, drag.nextSibling];
    ghost = drag.cloneNode(true); ghost.removeAttribute('data-dd-item'); ghost.setAttribute('data-dd-ghost', ''); ghost.setAttribute('aria-hidden', 'true'); ghost.inert = true;
    ghost.querySelectorAll('[id]').forEach(n => n.removeAttribute('id'));
    Object.assign(ghost.style, { position: 'fixed', left: 0, top: 0, width: r.width + 'px', height: r.height + 'px', margin: 0, zIndex: 999, pointerEvents: 'none' });
    drag.after(ghost); drag.setAttribute('data-dragging', ''); root.setAttribute('data-dd-active', ''); document.body.style.cursor = 'grabbing';
  }

  // Keyboard and screen reader mode: the handle is a toggle button (aria-pressed) that picks the item up.
  const finish = msg => { kb.removeAttribute('data-grabbed'); kbh.setAttribute('aria-pressed', 'false'); root.removeAttribute('data-dd-active'); clearOver(); kb = null; changed(); say(msg); };
  const move = fn => { moving = true; fn(); kbh.focus(); moving = false; kbh.scrollIntoView({ block: 'nearest' }); clearOver(kb.parentElement); kb.parentElement.setAttribute('data-over', ''); changed(); say(name(kb) + ' moved to ' + pos(kb) + '.'); };
  root.addEventListener('click', e => {
    const h = e.target.closest('[data-dd-handle]'); if (!h || dragged) return;
    const item = h.closest('[data-dd-item]');
    if (kb === item) return finish(name(item) + ' dropped, ' + pos(item) + '.');
    if (kb) finish(name(kb) + ' dropped, ' + pos(kb) + '.');
    kb = item; kbh = h; home = [item.parentElement, item.nextSibling];
    item.setAttribute('data-grabbed', ''); h.setAttribute('aria-pressed', 'true'); root.setAttribute('data-dd-active', ''); item.parentElement.setAttribute('data-over', ''); h.focus();
    say(name(item) + ' picked up, ' + pos(item) + '. Use the arrow keys to move it, Space to drop it, Escape to cancel.');
  });
  root.addEventListener('keydown', e => {
    if (!kb || e.target !== kbh) return;
    if (e.key === 'Escape') { e.preventDefault(); moving = true; putBack(kb); kbh.focus(); moving = false; return finish('Move cancelled. ' + name(kb) + ' returned to ' + pos(kb) + '.'); }
    const d = { ArrowUp: -1, ArrowLeft: -1, ArrowDown: 1, ArrowRight: 1 }[e.key]; if (!d) return;
    e.preventDefault();
    const l = kb.parentElement, all = items(l), i = all.indexOf(kb), ls = lists(l), side = e.key === 'ArrowLeft' || e.key === 'ArrowRight';
    if (side && ls.length > 1) {
      const t = ls[ls.indexOf(l) + d];
      if (!t) return say(name(kb) + ' is already in ' + l.getAttribute('aria-label') + ', the ' + (d < 0 ? 'first' : 'last') + ' list.');
      if (full(t)) return say(t.getAttribute('aria-label') + ' is full.');
      return move(() => t.insertBefore(kb, items(t)[i] || null));
    }
    const step = grid && !side ? d * all.filter(x => x.offsetTop === all[0].offsetTop).length : d, j = Math.max(0, Math.min(all.length - 1, i + step));
    if (j === i) return say(name(kb) + ' is already ' + (d < 0 ? 'first' : 'last') + '.');
    move(() => l.insertBefore(kb, j > i ? all[j].nextSibling : all[j]));
  });
  root.addEventListener('focusout', e => { if (kb && !moving && e.target === kbh) finish(name(kb) + ' dropped, ' + pos(kb) + '.'); });
  changed();
});

About this set

Drag items into targets rather than reordering one list. Assign people to teams starts with three unassigned people as pills with initials and roles, and three team boxes for Design, Engineering and Research, each with a limit of three, four or two people. Drop a person on a team and its count goes up; drop them on a full team and the box turns red and refuses. Plan a sprint against its capacity has a backlog and a sprint list of user stories with story points; as stories move, a bar and a line of text show how many of the twenty points are used and warn when the sprint is over capacity. Use them for assignment screens, seating plans, shopping and order builders, and planning tools. Every target is a list in the same group, and data-dd-max sets its limit. A whole team box, header included, takes the drop, so a small target is still easy to hit, and empty teams show a Drop people here slot. From the keyboard, Left and Right move a person or story to the previous or next target, and a full target is announced instead of being entered.

What’s included

  • People and stories move between several targets
  • Per-target limits with a red refusal state
  • Live counts, story point total and capacity bar
  • Whole team boxes act as drop targets
  • Left and Right keys carry an item to the next target

Accessibility

Every handle is a toggle button named Move plus the item name, with aria-pressed and the visible instructions joined through aria-describedby. Space or Enter picks the item up, the arrow keys move it, Space or Enter drops it and Escape puts it back, and each step is announced in an assertive aria-live region, for example "Felix Wu moved to Design, position 1 of 3.". Handles show a visible focus outline, and the tilt of the dragged copy and any transitions switch off under prefers-reduced-motion.

Customise it

Change --as-accent, --as-tint and --as-danger on .as; data-dd-max on a list sets its limit and data-capacity on the sprint list sets the points budget. Bootstrap uses .card, .badge and .progress; Tailwind uses the as colours in @theme.