Open demo

CSS code

HTML
<div class="gx"><span class="lbl">Handle styles</span>
  <p class="gx-hint" id="gx-hint1">Drag a row, or focus its handle, press Space and use the arrow keys.</p>
  <div class="gx-grid">
    <section class="gx-card gx-dots" data-dd data-ph="slot" aria-labelledby="gx-dots-t"><h3 id="gx-dots-t">Dot grip</h3><p>The familiar six-dot grip on the left.</p>
      <ul class="gx-list" data-dd-list="dots" aria-label="Playlist"><li class="gx-row" data-dd-item><button class="gx-h" type="button" data-dd-handle aria-describedby="gx-hint1"><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 data-dd-name>Northern Lights</span></li><li class="gx-row" data-dd-item><button class="gx-h" type="button" data-dd-handle aria-describedby="gx-hint1"><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 data-dd-name>Paper Planes</span></li><li class="gx-row" data-dd-item><button class="gx-h" type="button" data-dd-handle aria-describedby="gx-hint1"><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 data-dd-name>Slow Tide</span></li></ul>
      <p class="sr-only" aria-live="assertive" aria-atomic="true" data-dd-live></p></section>
    <section class="gx-card gx-bars" data-dd data-ph="slot" aria-labelledby="gx-bars-t"><h3 id="gx-bars-t">Bars on the right</h3><p>Keeps the start of each row for content.</p>
      <ul class="gx-list" data-dd-list="bars" aria-label="Checkout steps"><li class="gx-row" data-dd-item><span data-dd-name>Shipping</span><button class="gx-h" type="button" data-dd-handle aria-describedby="gx-hint1"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M3 6h14v2H3zM3 12h14v2H3z"/></svg></button></li><li class="gx-row" data-dd-item><span data-dd-name>Payment</span><button class="gx-h" type="button" data-dd-handle aria-describedby="gx-hint1"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M3 6h14v2H3zM3 12h14v2H3z"/></svg></button></li><li class="gx-row" data-dd-item><span data-dd-name>Review</span><button class="gx-h" type="button" data-dd-handle aria-describedby="gx-hint1"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M3 6h14v2H3zM3 12h14v2H3z"/></svg></button></li></ul>
      <p class="sr-only" aria-live="assertive" aria-atomic="true" data-dd-live></p></section>
    <section class="gx-card gx-strip" data-dd data-ph="slot" aria-labelledby="gx-strip-t"><h3 id="gx-strip-t">Edge strip</h3><p>A full-height strip: a big target for touch.</p>
      <ul class="gx-list" data-dd-list="strip" aria-label="Homepage blocks"><li class="gx-row" data-dd-item><button class="gx-h" type="button" data-dd-handle aria-describedby="gx-hint1"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="8" cy="4" r="1.4"/><circle cx="12" cy="4" r="1.4"/><circle cx="8" cy="8" r="1.4"/><circle cx="12" cy="8" r="1.4"/><circle cx="8" cy="12" r="1.4"/><circle cx="12" cy="12" r="1.4"/><circle cx="8" cy="16" r="1.4"/><circle cx="12" cy="16" r="1.4"/></svg></button><span data-dd-name>Hero</span></li><li class="gx-row" data-dd-item><button class="gx-h" type="button" data-dd-handle aria-describedby="gx-hint1"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="8" cy="4" r="1.4"/><circle cx="12" cy="4" r="1.4"/><circle cx="8" cy="8" r="1.4"/><circle cx="12" cy="8" r="1.4"/><circle cx="8" cy="12" r="1.4"/><circle cx="12" cy="12" r="1.4"/><circle cx="8" cy="16" r="1.4"/><circle cx="12" cy="16" r="1.4"/></svg></button><span data-dd-name>Customer logos</span></li><li class="gx-row" data-dd-item><button class="gx-h" type="button" data-dd-handle aria-describedby="gx-hint1"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="8" cy="4" r="1.4"/><circle cx="12" cy="4" r="1.4"/><circle cx="8" cy="8" r="1.4"/><circle cx="12" cy="8" r="1.4"/><circle cx="8" cy="12" r="1.4"/><circle cx="12" cy="12" r="1.4"/><circle cx="8" cy="16" r="1.4"/><circle cx="12" cy="16" r="1.4"/></svg></button><span data-dd-name>Pricing</span></li></ul>
      <p class="sr-only" aria-live="assertive" aria-atomic="true" data-dd-live></p></section>
    <section class="gx-card gx-move" data-dd data-dd-handle-only data-ph="slot" aria-labelledby="gx-move-t"><h3 id="gx-move-t">Handle only</h3><p>Rows ignore the pointer, so text stays selectable.</p>
      <ul class="gx-list" data-dd-list="move" aria-label="Signature lines"><li class="gx-row" data-dd-item><button class="gx-h" type="button" data-dd-handle aria-describedby="gx-hint1"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M10 2 7 5h2v4H5V7L2 10l3 3v-2h4v4H7l3 3 3-3h-2v-4h4v2l3-3-3-3v2h-4V5h2z"/></svg></button><span data-dd-name>Mara Jensen</span></li><li class="gx-row" data-dd-item><button class="gx-h" type="button" data-dd-handle aria-describedby="gx-hint1"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M10 2 7 5h2v4H5V7L2 10l3 3v-2h4v4H7l3 3 3-3h-2v-4h4v2l3-3-3-3v2h-4V5h2z"/></svg></button><span data-dd-name>Head of Design</span></li><li class="gx-row" data-dd-item><button class="gx-h" type="button" data-dd-handle aria-describedby="gx-hint1"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M10 2 7 5h2v4H5V7L2 10l3 3v-2h4v4H7l3 3 3-3h-2v-4h4v2l3-3-3-3v2h-4V5h2z"/></svg></button><span data-dd-name>[email protected]</span></li></ul>
      <p class="sr-only" aria-live="assertive" aria-atomic="true" data-dd-live></p></section>
  </div></div>

<div class="gx"><span class="lbl">Placeholder styles</span>
  <div class="gx-grid">
    <section class="gx-card" data-dd data-ph="slot" aria-labelledby="gx-slot-t"><h3 id="gx-slot-t">Dashed slot</h3><p>Holds the exact size of the item being moved.</p>
      <span class="gx-tag" aria-hidden="true">Mid-drag</span><div class="gx-preview" data-ph="slot" aria-hidden="true"><div class="gx-row"><span>Intro video</span></div><div class="gx-row" data-dragging><span>Lesson 1: Setup</span></div><div class="gx-row"><span>Lesson 2: Layout</span></div><div class="gx-row gx-ghost"><span>Lesson 1: Setup</span></div></div><span class="gx-tag" aria-hidden="true">Try it</span>
      <ul class="gx-list" data-dd-list="slot" aria-label="Course outline, dashed slot"><li class="gx-row" data-dd-item><button class="gx-h" type="button" data-dd-handle aria-describedby="gx-hint1"><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 data-dd-name>Intro video</span></li><li class="gx-row" data-dd-item><button class="gx-h" type="button" data-dd-handle aria-describedby="gx-hint1"><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 data-dd-name>Lesson 1: Setup</span></li><li class="gx-row" data-dd-item><button class="gx-h" type="button" data-dd-handle aria-describedby="gx-hint1"><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 data-dd-name>Lesson 2: Layout</span></li></ul>
      <p class="sr-only" aria-live="assertive" aria-atomic="true" data-dd-live></p></section>
    <section class="gx-card" data-dd data-ph="line" aria-labelledby="gx-line-t"><h3 id="gx-line-t">Insertion line</h3><p>Collapses to a line, so the list barely shifts.</p>
      <span class="gx-tag" aria-hidden="true">Mid-drag</span><div class="gx-preview" data-ph="line" aria-hidden="true"><div class="gx-row"><span>Intro video</span></div><div class="gx-row" data-dragging><span>Lesson 1: Setup</span></div><div class="gx-row"><span>Lesson 2: Layout</span></div><div class="gx-row gx-ghost"><span>Lesson 1: Setup</span></div></div><span class="gx-tag" aria-hidden="true">Try it</span>
      <ul class="gx-list" data-dd-list="line" aria-label="Course outline, insertion line"><li class="gx-row" data-dd-item><button class="gx-h" type="button" data-dd-handle aria-describedby="gx-hint1"><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 data-dd-name>Intro video</span></li><li class="gx-row" data-dd-item><button class="gx-h" type="button" data-dd-handle aria-describedby="gx-hint1"><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 data-dd-name>Lesson 1: Setup</span></li><li class="gx-row" data-dd-item><button class="gx-h" type="button" data-dd-handle aria-describedby="gx-hint1"><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 data-dd-name>Lesson 2: Layout</span></li></ul>
      <p class="sr-only" aria-live="assertive" aria-atomic="true" data-dd-live></p></section>
    <section class="gx-card" data-dd data-ph="fade" aria-labelledby="gx-fade-t"><h3 id="gx-fade-t">Faded original</h3><p>Leaves the item in place at low opacity.</p>
      <span class="gx-tag" aria-hidden="true">Mid-drag</span><div class="gx-preview" data-ph="fade" aria-hidden="true"><div class="gx-row"><span>Intro video</span></div><div class="gx-row" data-dragging><span>Lesson 1: Setup</span></div><div class="gx-row"><span>Lesson 2: Layout</span></div><div class="gx-row gx-ghost"><span>Lesson 1: Setup</span></div></div><span class="gx-tag" aria-hidden="true">Try it</span>
      <ul class="gx-list" data-dd-list="fade" aria-label="Course outline, faded original"><li class="gx-row" data-dd-item><button class="gx-h" type="button" data-dd-handle aria-describedby="gx-hint1"><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 data-dd-name>Intro video</span></li><li class="gx-row" data-dd-item><button class="gx-h" type="button" data-dd-handle aria-describedby="gx-hint1"><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 data-dd-name>Lesson 1: Setup</span></li><li class="gx-row" data-dd-item><button class="gx-h" type="button" data-dd-handle aria-describedby="gx-hint1"><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 data-dd-name>Lesson 2: Layout</span></li></ul>
      <p class="sr-only" aria-live="assertive" aria-atomic="true" data-dd-live></p></section>
  </div></div>

<div><span class="lbl">Drop zone states</span>
<section class="gx gx-card" data-dd="wrap" aria-labelledby="gx-z-t">
  <h3 id="gx-z-t" class="sr-only">Drop zone states</h3>
  <div class="gx-states">
    <div class="gx-zone"><b>Idle</b>No drag in progress.</div>
    <div class="gx-zone is-ready"><b>Ready</b>A drag started: every valid target shows a dashed outline.</div>
    <div class="gx-zone is-over"><b>Over</b>The item is above this target and will land here.</div>
    <div class="gx-zone is-full"><b>Full</b>The target is at its limit and refuses the item.</div>
  </div>
  <p class="gx-hint" id="gx-hint3">Drag a file into a folder, or focus its handle, press Space and use Left and Right. Shared holds 2 files.</p>
  <div class="gx-folders">
    <div class="gx-zone gx-target" data-dd-zone><div class="gx-fh">Inbox<span><span data-dd-count="gx-inbox">3</span></span></div><ul class="gx-files" id="gx-inbox" data-dd-list="files" aria-label="Inbox"><li class="gx-file" data-dd-item><button class="gx-h" type="button" data-dd-handle aria-describedby="gx-hint3"><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><svg class="gx-ico" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M5 2.5h6.5L15 6v11.5H5zM11 2.5V6.5h4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg><span data-dd-name>brief-v3.pdf</span></li><li class="gx-file" data-dd-item><button class="gx-h" type="button" data-dd-handle aria-describedby="gx-hint3"><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><svg class="gx-ico" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M5 2.5h6.5L15 6v11.5H5zM11 2.5V6.5h4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg><span data-dd-name>logo.svg</span></li><li class="gx-file" data-dd-item><button class="gx-h" type="button" data-dd-handle aria-describedby="gx-hint3"><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><svg class="gx-ico" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M5 2.5h6.5L15 6v11.5H5zM11 2.5V6.5h4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg><span data-dd-name>meeting-notes.md</span></li></ul></div>
    <div class="gx-zone gx-target" data-dd-zone><div class="gx-fh">Archive<span><span data-dd-count="gx-archive">0</span></span></div><ul class="gx-files" id="gx-archive" data-dd-list="files" aria-label="Archive"></ul></div>
    <div class="gx-zone gx-target" data-dd-zone><div class="gx-fh">Shared<span><span data-dd-count="gx-shared">1</span> / 2</span></div><ul class="gx-files" id="gx-shared" data-dd-list="files" data-dd-max="2" aria-label="Shared"><li class="gx-file" data-dd-item><button class="gx-h" type="button" data-dd-handle aria-describedby="gx-hint3"><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><svg class="gx-ico" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M5 2.5h6.5L15 6v11.5H5zM11 2.5V6.5h4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg><span data-dd-name>budget.xlsx</span></li></ul></div>
  </div>
  <p class="sr-only" aria-live="assertive" aria-atomic="true" data-dd-live></p>
</section></div>
CSS
/* Handles, placeholders and drop-zone states. Every list runs on the same script; the look comes from
   data-dd-handle buttons, the root's data-ph (slot, line or fade) and the data-dd-active / data-over / data-full states. */
.gx{--gx-accent:#be185d;--gx-tint:#fdf0f6;--gx-line:#e4e7ec;--gx-muted:#5d6679;--gx-ink:#101828;--gx-danger:#b42318;color:var(--gx-ink)}
.gx-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:12px}
.gx-card{background:#fff;border:1px solid var(--gx-line);border-radius:14px;padding:14px;box-shadow:0 1px 2px rgba(16,24,40,.05)}
.gx-card h3{font-size:.88rem}
.gx-card>p{font-size:.78rem;color:var(--gx-muted);margin:2px 0 10px}
.gx-hint{font-size:.8rem;color:var(--gx-muted);margin-bottom:10px}
.gx-list{list-style:none;display:grid;grid-template-columns:minmax(0,1fr);gap:6px}
.gx-row{display:flex;align-items:center;gap:8px;min-height:40px;padding:4px 10px 4px 4px;background:#fff;border:1px solid var(--gx-line);border-radius:10px;font-size:.86rem;font-weight:600;cursor:grab;user-select:none}
.gx-row>span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gx-h{flex:none;display:grid;place-items:center;width:28px;height:30px;border:0;border-radius:7px;background:none;color:#667085;cursor:grab;touch-action:none}
.gx-h:hover{background:#f2f4f7;color:var(--gx-ink)}
.gx-h:focus-visible{outline:2px solid var(--gx-accent);outline-offset:1px}
.gx-h svg{width:16px;height:16px}
/* handle variants */
.gx-bars .gx-row{padding:4px 4px 4px 12px}
.gx-strip .gx-row{padding:0 10px 0 0;overflow:hidden}
.gx-strip .gx-h{align-self:stretch;width:22px;height:auto;border-radius:0;background:var(--gx-tint);color:var(--gx-accent);border-right:1px solid #f6cfe0}
.gx-strip .gx-h:focus-visible{outline-offset:-3px}
.gx-move .gx-row{cursor:auto;user-select:text}
.gx-move .gx-h{cursor:move;border:1px solid #d0d5dd}
/* placeholder variants */
.gx-row[data-dd-ghost]{box-shadow:0 14px 30px -10px rgba(16,24,40,.4);rotate:-1deg}
[data-ph="slot"] [data-dragging]{background:var(--gx-tint);border:2px dashed #e68bb4}
[data-ph="slot"] [data-dragging]>*{visibility:hidden}
[data-ph="line"] [data-dragging]{position:relative;min-height:0;height:4px;padding:0;margin:3px 0;border:0;border-radius:2px;background:var(--gx-accent);overflow:visible}
[data-ph="line"] [data-dragging]>*{display:none}
[data-ph="line"] [data-dragging]::before{content:"";position:absolute;left:-2px;top:-4px;width:12px;height:12px;border-radius:50%;background:#fff;border:3px solid var(--gx-accent);box-sizing:border-box}
[data-ph="fade"] [data-dragging]{opacity:.6;border-style:dashed;border-color:#98a2b3}
.gx-preview{position:relative;display:grid;gap:6px;padding:10px 10px 54px;margin-bottom:10px;border-radius:10px;background:#f8f9fb;border:1px solid var(--gx-line)}
.gx-preview .gx-row{cursor:default;padding-left:12px}
.gx-preview .gx-ghost{position:absolute;left:34px;right:-6px;bottom:10px;background:#fff;box-shadow:0 14px 30px -10px rgba(16,24,40,.4);rotate:-1.5deg}
.gx-tag{display:inline-block;font-size:.66rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:var(--gx-muted);margin-bottom:6px}
.gx-row[data-grabbed]{border-color:var(--gx-accent);box-shadow:0 0 0 2px var(--gx-accent)}
/* drop-zone states */
.gx-states{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-bottom:14px}
.gx-zone{display:grid;align-content:start;gap:2px;min-height:86px;padding:10px 12px;border-radius:12px;background:#f8f9fb;outline:2px dashed transparent;outline-offset:-2px;border:1px solid var(--gx-line);font-size:.78rem;color:var(--gx-muted);transition:background-color .15s,outline-color .15s}
.gx-zone b{font-size:.84rem;color:var(--gx-ink)}
.gx-zone.is-ready,.gx[data-dd-active] .gx-target{outline-color:#98a2b3;border-color:transparent}
.gx-zone.is-over,.gx .gx-target:has(>[data-over]){outline-style:solid;outline-color:var(--gx-accent);background:var(--gx-tint);border-color:transparent}
.gx-zone.is-full,.gx .gx-target:has(>[data-over][data-full]):not(:has([data-dragging],[data-grabbed])){outline-style:solid;outline-color:var(--gx-danger);background:#fef3f2;border-color:transparent}
.gx-zone.is-full b{color:var(--gx-danger)}
.gx-folders{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px}
.gx-fh{display:flex;justify-content:space-between;gap:8px;font-size:.84rem;font-weight:700;margin-bottom:6px}
.gx-fh span{font-size:.75rem;color:var(--gx-muted);font-variant-numeric:tabular-nums}
.gx-files{list-style:none;display:flex;flex-wrap:wrap;align-content:flex-start;gap:6px;min-height:40px}
.gx-files[data-empty]::after{content:"Empty folder";align-self:center;font-size:.78rem;color:var(--gx-muted)}
.gx-file{display:flex;align-items:center;gap:4px;padding:2px 10px 2px 2px;background:#fff;border:1px solid #d0d5dd;border-radius:8px;font-size:.8rem;font-weight:600;cursor:grab;user-select:none}
.gx-file .gx-ico{width:16px;height:16px;color:var(--gx-accent)}
.gx-file[data-dragging]{background:var(--gx-tint);border:2px dashed #e68bb4}
.gx-file[data-dragging]>*{visibility:hidden}
.gx-file[data-dd-ghost]{box-shadow:0 14px 30px -10px rgba(16,24,40,.4);rotate:-2deg}
.gx-file[data-grabbed]{border-color:var(--gx-accent);box-shadow:0 0 0 2px var(--gx-accent)}
.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 (prefers-reduced-motion:reduce){.gx-zone{transition:none}.gx-row[data-dd-ghost],.gx-file[data-dd-ghost],.gx-preview .gx-ghost{rotate:none}}
JavaScript
// 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 class="gx"><span class="lbl">Handle styles</span>
  <p class="gx-hint text-body-secondary mb-2" id="gx-hint1">Drag a row, or focus its handle, press Space and use the arrow keys.</p>
  <div class="row row-cols-1 row-cols-sm-2 row-cols-lg-4 g-3">
    <div class="col"><section class="card h-100 gx-dots" data-dd data-ph="slot" aria-labelledby="gx-dots-t"><h3 class="fw-bold mb-0" id="gx-dots-t">Dot grip</h3><p class="text-body-secondary">The familiar six-dot grip on the left.</p>
      <ul class="gx-list d-grid gap-2 list-unstyled mb-0" data-dd-list="dots" aria-label="Playlist"><li class="gx-row d-flex align-items-center gap-2 fw-semibold" data-dd-item><button class="gx-h btn d-grid flex-shrink-0" type="button" data-dd-handle aria-describedby="gx-hint1"><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="text-truncate" data-dd-name>Northern Lights</span></li><li class="gx-row d-flex align-items-center gap-2 fw-semibold" data-dd-item><button class="gx-h btn d-grid flex-shrink-0" type="button" data-dd-handle aria-describedby="gx-hint1"><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="text-truncate" data-dd-name>Paper Planes</span></li><li class="gx-row d-flex align-items-center gap-2 fw-semibold" data-dd-item><button class="gx-h btn d-grid flex-shrink-0" type="button" data-dd-handle aria-describedby="gx-hint1"><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="text-truncate" data-dd-name>Slow Tide</span></li></ul>
      <p class="visually-hidden" aria-live="assertive" aria-atomic="true" data-dd-live></p></section></div>
    <div class="col"><section class="card h-100 gx-bars" data-dd data-ph="slot" aria-labelledby="gx-bars-t"><h3 class="fw-bold mb-0" id="gx-bars-t">Bars on the right</h3><p class="text-body-secondary">Keeps the start of each row for content.</p>
      <ul class="gx-list d-grid gap-2 list-unstyled mb-0" data-dd-list="bars" aria-label="Checkout steps"><li class="gx-row d-flex align-items-center gap-2 fw-semibold" data-dd-item><span class="text-truncate" data-dd-name>Shipping</span><button class="gx-h btn d-grid flex-shrink-0" type="button" data-dd-handle aria-describedby="gx-hint1"><svg class="m-auto" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M3 6h14v2H3zM3 12h14v2H3z"/></svg></button></li><li class="gx-row d-flex align-items-center gap-2 fw-semibold" data-dd-item><span class="text-truncate" data-dd-name>Payment</span><button class="gx-h btn d-grid flex-shrink-0" type="button" data-dd-handle aria-describedby="gx-hint1"><svg class="m-auto" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M3 6h14v2H3zM3 12h14v2H3z"/></svg></button></li><li class="gx-row d-flex align-items-center gap-2 fw-semibold" data-dd-item><span class="text-truncate" data-dd-name>Review</span><button class="gx-h btn d-grid flex-shrink-0" type="button" data-dd-handle aria-describedby="gx-hint1"><svg class="m-auto" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M3 6h14v2H3zM3 12h14v2H3z"/></svg></button></li></ul>
      <p class="visually-hidden" aria-live="assertive" aria-atomic="true" data-dd-live></p></section></div>
    <div class="col"><section class="card h-100 gx-strip" data-dd data-ph="slot" aria-labelledby="gx-strip-t"><h3 class="fw-bold mb-0" id="gx-strip-t">Edge strip</h3><p class="text-body-secondary">A full-height strip: a big target for touch.</p>
      <ul class="gx-list d-grid gap-2 list-unstyled mb-0" data-dd-list="strip" aria-label="Homepage blocks"><li class="gx-row d-flex align-items-center gap-2 fw-semibold" data-dd-item><button class="gx-h btn d-grid flex-shrink-0" type="button" data-dd-handle aria-describedby="gx-hint1"><svg class="m-auto" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="8" cy="4" r="1.4"/><circle cx="12" cy="4" r="1.4"/><circle cx="8" cy="8" r="1.4"/><circle cx="12" cy="8" r="1.4"/><circle cx="8" cy="12" r="1.4"/><circle cx="12" cy="12" r="1.4"/><circle cx="8" cy="16" r="1.4"/><circle cx="12" cy="16" r="1.4"/></svg></button><span class="text-truncate" data-dd-name>Hero</span></li><li class="gx-row d-flex align-items-center gap-2 fw-semibold" data-dd-item><button class="gx-h btn d-grid flex-shrink-0" type="button" data-dd-handle aria-describedby="gx-hint1"><svg class="m-auto" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="8" cy="4" r="1.4"/><circle cx="12" cy="4" r="1.4"/><circle cx="8" cy="8" r="1.4"/><circle cx="12" cy="8" r="1.4"/><circle cx="8" cy="12" r="1.4"/><circle cx="12" cy="12" r="1.4"/><circle cx="8" cy="16" r="1.4"/><circle cx="12" cy="16" r="1.4"/></svg></button><span class="text-truncate" data-dd-name>Customer logos</span></li><li class="gx-row d-flex align-items-center gap-2 fw-semibold" data-dd-item><button class="gx-h btn d-grid flex-shrink-0" type="button" data-dd-handle aria-describedby="gx-hint1"><svg class="m-auto" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="8" cy="4" r="1.4"/><circle cx="12" cy="4" r="1.4"/><circle cx="8" cy="8" r="1.4"/><circle cx="12" cy="8" r="1.4"/><circle cx="8" cy="12" r="1.4"/><circle cx="12" cy="12" r="1.4"/><circle cx="8" cy="16" r="1.4"/><circle cx="12" cy="16" r="1.4"/></svg></button><span class="text-truncate" data-dd-name>Pricing</span></li></ul>
      <p class="visually-hidden" aria-live="assertive" aria-atomic="true" data-dd-live></p></section></div>
    <div class="col"><section class="card h-100 gx-move" data-dd data-dd-handle-only data-ph="slot" aria-labelledby="gx-move-t"><h3 class="fw-bold mb-0" id="gx-move-t">Handle only</h3><p class="text-body-secondary">Rows ignore the pointer, so text stays selectable.</p>
      <ul class="gx-list d-grid gap-2 list-unstyled mb-0" data-dd-list="move" aria-label="Signature lines"><li class="gx-row d-flex align-items-center gap-2 fw-semibold" data-dd-item><button class="gx-h btn d-grid flex-shrink-0" type="button" data-dd-handle aria-describedby="gx-hint1"><svg class="m-auto" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M10 2 7 5h2v4H5V7L2 10l3 3v-2h4v4H7l3 3 3-3h-2v-4h4v2l3-3-3-3v2h-4V5h2z"/></svg></button><span class="text-truncate" data-dd-name>Mara Jensen</span></li><li class="gx-row d-flex align-items-center gap-2 fw-semibold" data-dd-item><button class="gx-h btn d-grid flex-shrink-0" type="button" data-dd-handle aria-describedby="gx-hint1"><svg class="m-auto" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M10 2 7 5h2v4H5V7L2 10l3 3v-2h4v4H7l3 3 3-3h-2v-4h4v2l3-3-3-3v2h-4V5h2z"/></svg></button><span class="text-truncate" data-dd-name>Head of Design</span></li><li class="gx-row d-flex align-items-center gap-2 fw-semibold" data-dd-item><button class="gx-h btn d-grid flex-shrink-0" type="button" data-dd-handle aria-describedby="gx-hint1"><svg class="m-auto" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M10 2 7 5h2v4H5V7L2 10l3 3v-2h4v4H7l3 3 3-3h-2v-4h4v2l3-3-3-3v2h-4V5h2z"/></svg></button><span class="text-truncate" data-dd-name>[email protected]</span></li></ul>
      <p class="visually-hidden" aria-live="assertive" aria-atomic="true" data-dd-live></p></section></div>
  </div></div>

<div class="gx"><span class="lbl">Placeholder styles</span>
  <div class="row row-cols-1 row-cols-md-3 g-3">
    <div class="col"><section class="card h-100" data-dd data-ph="slot" aria-labelledby="gx-slot-t"><h3 class="fw-bold mb-0" id="gx-slot-t">Dashed slot</h3><p class="text-body-secondary">Holds the exact size of the item being moved.</p>
      <span class="gx-tag d-block fw-bolder text-uppercase text-body-secondary" aria-hidden="true">Mid-drag</span><div class="gx-preview d-grid gap-2" data-ph="slot" aria-hidden="true"><div class="gx-row d-flex align-items-center fw-semibold"><span>Intro video</span></div><div class="gx-row d-flex align-items-center fw-semibold" data-dragging><span>Lesson 1: Setup</span></div><div class="gx-row d-flex align-items-center fw-semibold"><span>Lesson 2: Layout</span></div><div class="gx-row gx-ghost d-flex align-items-center fw-semibold"><span>Lesson 1: Setup</span></div></div><span class="gx-tag d-block fw-bolder text-uppercase text-body-secondary" aria-hidden="true">Try it</span>
      <ul class="gx-list d-grid gap-2 list-unstyled mb-0" data-dd-list="slot" aria-label="Course outline, dashed slot"><li class="gx-row d-flex align-items-center gap-2 fw-semibold" data-dd-item><button class="gx-h btn d-grid flex-shrink-0" type="button" data-dd-handle aria-describedby="gx-hint1"><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="text-truncate" data-dd-name>Intro video</span></li><li class="gx-row d-flex align-items-center gap-2 fw-semibold" data-dd-item><button class="gx-h btn d-grid flex-shrink-0" type="button" data-dd-handle aria-describedby="gx-hint1"><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="text-truncate" data-dd-name>Lesson 1: Setup</span></li><li class="gx-row d-flex align-items-center gap-2 fw-semibold" data-dd-item><button class="gx-h btn d-grid flex-shrink-0" type="button" data-dd-handle aria-describedby="gx-hint1"><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="text-truncate" data-dd-name>Lesson 2: Layout</span></li></ul>
      <p class="visually-hidden" aria-live="assertive" aria-atomic="true" data-dd-live></p></section></div>
    <div class="col"><section class="card h-100" data-dd data-ph="line" aria-labelledby="gx-line-t"><h3 class="fw-bold mb-0" id="gx-line-t">Insertion line</h3><p class="text-body-secondary">Collapses to a line, so the list barely shifts.</p>
      <span class="gx-tag d-block fw-bolder text-uppercase text-body-secondary" aria-hidden="true">Mid-drag</span><div class="gx-preview d-grid gap-2" data-ph="line" aria-hidden="true"><div class="gx-row d-flex align-items-center fw-semibold"><span>Intro video</span></div><div class="gx-row d-flex align-items-center fw-semibold" data-dragging><span>Lesson 1: Setup</span></div><div class="gx-row d-flex align-items-center fw-semibold"><span>Lesson 2: Layout</span></div><div class="gx-row gx-ghost d-flex align-items-center fw-semibold"><span>Lesson 1: Setup</span></div></div><span class="gx-tag d-block fw-bolder text-uppercase text-body-secondary" aria-hidden="true">Try it</span>
      <ul class="gx-list d-grid gap-2 list-unstyled mb-0" data-dd-list="line" aria-label="Course outline, insertion line"><li class="gx-row d-flex align-items-center gap-2 fw-semibold" data-dd-item><button class="gx-h btn d-grid flex-shrink-0" type="button" data-dd-handle aria-describedby="gx-hint1"><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="text-truncate" data-dd-name>Intro video</span></li><li class="gx-row d-flex align-items-center gap-2 fw-semibold" data-dd-item><button class="gx-h btn d-grid flex-shrink-0" type="button" data-dd-handle aria-describedby="gx-hint1"><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="text-truncate" data-dd-name>Lesson 1: Setup</span></li><li class="gx-row d-flex align-items-center gap-2 fw-semibold" data-dd-item><button class="gx-h btn d-grid flex-shrink-0" type="button" data-dd-handle aria-describedby="gx-hint1"><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="text-truncate" data-dd-name>Lesson 2: Layout</span></li></ul>
      <p class="visually-hidden" aria-live="assertive" aria-atomic="true" data-dd-live></p></section></div>
    <div class="col"><section class="card h-100" data-dd data-ph="fade" aria-labelledby="gx-fade-t"><h3 class="fw-bold mb-0" id="gx-fade-t">Faded original</h3><p class="text-body-secondary">Leaves the item in place at low opacity.</p>
      <span class="gx-tag d-block fw-bolder text-uppercase text-body-secondary" aria-hidden="true">Mid-drag</span><div class="gx-preview d-grid gap-2" data-ph="fade" aria-hidden="true"><div class="gx-row d-flex align-items-center fw-semibold"><span>Intro video</span></div><div class="gx-row d-flex align-items-center fw-semibold" data-dragging><span>Lesson 1: Setup</span></div><div class="gx-row d-flex align-items-center fw-semibold"><span>Lesson 2: Layout</span></div><div class="gx-row gx-ghost d-flex align-items-center fw-semibold"><span>Lesson 1: Setup</span></div></div><span class="gx-tag d-block fw-bolder text-uppercase text-body-secondary" aria-hidden="true">Try it</span>
      <ul class="gx-list d-grid gap-2 list-unstyled mb-0" data-dd-list="fade" aria-label="Course outline, faded original"><li class="gx-row d-flex align-items-center gap-2 fw-semibold" data-dd-item><button class="gx-h btn d-grid flex-shrink-0" type="button" data-dd-handle aria-describedby="gx-hint1"><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="text-truncate" data-dd-name>Intro video</span></li><li class="gx-row d-flex align-items-center gap-2 fw-semibold" data-dd-item><button class="gx-h btn d-grid flex-shrink-0" type="button" data-dd-handle aria-describedby="gx-hint1"><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="text-truncate" data-dd-name>Lesson 1: Setup</span></li><li class="gx-row d-flex align-items-center gap-2 fw-semibold" data-dd-item><button class="gx-h btn d-grid flex-shrink-0" type="button" data-dd-handle aria-describedby="gx-hint1"><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="text-truncate" data-dd-name>Lesson 2: Layout</span></li></ul>
      <p class="visually-hidden" aria-live="assertive" aria-atomic="true" data-dd-live></p></section></div>
  </div></div>

<div><span class="lbl">Drop zone states</span>
<section class="gx card" data-dd="wrap" aria-labelledby="gx-z-t">
  <h3 id="gx-z-t" class="visually-hidden">Drop zone states</h3>
  <div class="row row-cols-2 row-cols-md-4 g-2 mb-3">
    <div class="col"><div class="gx-zone d-grid align-content-start gap-1 h-100 text-body-secondary"><b>Idle</b>No drag in progress.</div></div>
    <div class="col"><div class="gx-zone d-grid align-content-start gap-1 h-100 text-body-secondary is-ready"><b>Ready</b>A drag started: every valid target shows a dashed outline.</div></div>
    <div class="col"><div class="gx-zone d-grid align-content-start gap-1 h-100 text-body-secondary is-over"><b>Over</b>The item is above this target and will land here.</div></div>
    <div class="col"><div class="gx-zone d-grid align-content-start gap-1 h-100 text-body-secondary is-full"><b>Full</b>The target is at its limit and refuses the item.</div></div>
  </div>
  <p class="gx-hint text-body-secondary mb-2" id="gx-hint3">Drag a file into a folder, or focus its handle, press Space and use Left and Right. Shared holds 2 files.</p>
  <div class="row row-cols-1 row-cols-sm-3 g-2">
    <div class="col"><div class="gx-zone gx-target h-100" data-dd-zone><div class="gx-fh d-flex justify-content-between gap-2 fw-bold mb-2">Inbox<span class="badge rounded-pill"><span data-dd-count="gx-inbox">3</span></span></div><ul class="gx-files d-flex flex-wrap gap-2 list-unstyled mb-0" id="gx-inbox" data-dd-list="files" aria-label="Inbox"><li class="gx-file d-flex align-items-center gap-1 fw-semibold" data-dd-item><button class="gx-h btn d-grid" type="button" data-dd-handle aria-describedby="gx-hint3"><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><svg class="gx-ico" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M5 2.5h6.5L15 6v11.5H5zM11 2.5V6.5h4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg><span data-dd-name>brief-v3.pdf</span></li><li class="gx-file d-flex align-items-center gap-1 fw-semibold" data-dd-item><button class="gx-h btn d-grid" type="button" data-dd-handle aria-describedby="gx-hint3"><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><svg class="gx-ico" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M5 2.5h6.5L15 6v11.5H5zM11 2.5V6.5h4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg><span data-dd-name>logo.svg</span></li><li class="gx-file d-flex align-items-center gap-1 fw-semibold" data-dd-item><button class="gx-h btn d-grid" type="button" data-dd-handle aria-describedby="gx-hint3"><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><svg class="gx-ico" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M5 2.5h6.5L15 6v11.5H5zM11 2.5V6.5h4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg><span data-dd-name>meeting-notes.md</span></li></ul></div></div>
    <div class="col"><div class="gx-zone gx-target h-100" data-dd-zone><div class="gx-fh d-flex justify-content-between gap-2 fw-bold mb-2">Archive<span class="badge rounded-pill"><span data-dd-count="gx-archive">0</span></span></div><ul class="gx-files d-flex flex-wrap gap-2 list-unstyled mb-0" id="gx-archive" data-dd-list="files" aria-label="Archive"></ul></div></div>
    <div class="col"><div class="gx-zone gx-target h-100" data-dd-zone><div class="gx-fh d-flex justify-content-between gap-2 fw-bold mb-2">Shared<span class="badge rounded-pill"><span data-dd-count="gx-shared">1</span> / 2</span></div><ul class="gx-files d-flex flex-wrap gap-2 list-unstyled mb-0" id="gx-shared" data-dd-list="files" data-dd-max="2" aria-label="Shared"><li class="gx-file d-flex align-items-center gap-1 fw-semibold" data-dd-item><button class="gx-h btn d-grid" type="button" data-dd-handle aria-describedby="gx-hint3"><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><svg class="gx-ico" width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M5 2.5h6.5L15 6v11.5H5zM11 2.5V6.5h4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg><span data-dd-name>budget.xlsx</span></li></ul></div></div>
  </div>
  <p class="visually-hidden" aria-live="assertive" aria-atomic="true" data-dd-live></p>
</section></div>
CSS
/* Handles, placeholders and drop-zone states for Bootstrap 5: .card panels in a .row grid, .btn handles and
   .badge counts; these rules hold the three placeholder looks (data-ph) and the target states. */
.gx{--bs-secondary-color:#5d6679;--gx-accent:#be185d;--gx-tint:#fdf0f6;--gx-danger:#b42318;color:#101828}
.gx .card{--bs-card-border-color:#e4e7ec;--bs-card-border-radius:14px;padding:14px;box-shadow:0 1px 2px rgba(16,24,40,.05)}
.gx h3{font-size:.88rem}
.gx .card>p{font-size:.78rem;margin:2px 0 10px}
.gx-hint{font-size:.8rem}
.gx-row{min-height:40px;padding:4px 10px 4px 4px;background:#fff;color:#101828;border:1px solid #e4e7ec;border-radius:10px;font-size:.86rem;cursor:grab;user-select:none}
.gx-row>span{flex:1;min-width:0}
.gx-list{grid-template-columns:minmax(0,1fr)}
.gx-h{--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:7px;width:28px;height:30px;cursor:grab;touch-action:none}
.gx-h:focus-visible{outline:2px solid var(--gx-accent);outline-offset:1px;box-shadow:none}
.gx-bars .gx-row{padding:4px 4px 4px 12px}
.gx-strip .gx-row{padding:0 10px 0 0;overflow:hidden}
.gx-strip .gx-h{--bs-btn-bg:var(--gx-tint);--bs-btn-color:var(--gx-accent);--bs-btn-hover-bg:#fbe3ee;--bs-btn-hover-color:var(--gx-accent);--bs-btn-border-radius:0;align-self:stretch;width:22px;height:auto;border-right:1px solid #f6cfe0}
.gx-strip .gx-h:focus-visible{outline-offset:-3px}
.gx-move .gx-row{cursor:auto;user-select:text}
.gx-move .gx-h{cursor:move;--bs-btn-border-width:1px;--bs-btn-border-color:#d0d5dd;--bs-btn-hover-border-color:#d0d5dd}
.gx-row[data-dd-ghost]{box-shadow:0 14px 30px -10px rgba(16,24,40,.4);rotate:-1deg}
[data-ph="slot"] [data-dragging]{background:var(--gx-tint);border:2px dashed #e68bb4}
[data-ph="slot"] [data-dragging]>*{visibility:hidden}
[data-ph="line"] [data-dragging]{position:relative;min-height:0;height:4px;padding:0;margin:3px 0;border:0;border-radius:2px;background:var(--gx-accent);overflow:visible}
[data-ph="line"] [data-dragging]>*{display:none!important}
[data-ph="line"] [data-dragging]::before{content:"";position:absolute;left:-2px;top:-4px;width:12px;height:12px;border-radius:50%;background:#fff;border:3px solid var(--gx-accent)}
[data-ph="fade"] [data-dragging]{opacity:.6;border-style:dashed;border-color:#98a2b3}
.gx-preview{position:relative;padding:10px 10px 54px;margin-bottom:10px;border-radius:10px;background:#f8f9fb;border:1px solid #e4e7ec}
.gx-preview .gx-row{cursor:default;padding-left:12px}
.gx-preview .gx-ghost{position:absolute;left:34px;right:-6px;bottom:10px;background:#fff;box-shadow:0 14px 30px -10px rgba(16,24,40,.4);rotate:-1.5deg}
.gx-tag{font-size:.66rem;letter-spacing:.06em;margin-bottom:6px}
.gx-row[data-grabbed]{border-color:var(--gx-accent);box-shadow:0 0 0 2px var(--gx-accent)}
.gx-zone{min-height:86px;padding:10px 12px;border-radius:12px;background:#f8f9fb;outline:2px dashed transparent;outline-offset:-2px;border:1px solid #e4e7ec;font-size:.78rem;transition:background-color .15s,outline-color .15s}
.gx-zone b{font-size:.84rem;color:#101828}
.gx-zone.is-ready,.gx[data-dd-active] .gx-target{outline-color:#98a2b3;border-color:transparent}
.gx-zone.is-over,.gx .gx-target:has(>[data-over]){outline-style:solid;outline-color:var(--gx-accent);background:var(--gx-tint);border-color:transparent}
.gx-zone.is-full,.gx .gx-target:has(>[data-over][data-full]):not(:has([data-dragging],[data-grabbed])){outline-style:solid;outline-color:var(--gx-danger);background:#fef3f2;border-color:transparent}
.gx-zone.is-full b{color:var(--gx-danger)}
.gx-fh{font-size:.84rem}
.gx-fh .badge{font-size:.72rem;background:#fff;color:#344054;border:1px solid #e4e7ec;font-variant-numeric:tabular-nums}
.gx-files{min-height:40px;align-content:flex-start}
.gx-files[data-empty]::after{content:"Empty folder";align-self:center;font-size:.78rem;color:#5d6679}
.gx-file{padding:2px 10px 2px 2px;background:#fff;border:1px solid #d0d5dd;border-radius:8px;font-size:.8rem;cursor:grab;user-select:none}
.gx-file .gx-ico{color:var(--gx-accent)}
.gx-file[data-dragging]{background:var(--gx-tint);border:2px dashed #e68bb4}
.gx-file[data-dragging]>*{visibility:hidden}
.gx-file[data-dd-ghost]{box-shadow:0 14px 30px -10px rgba(16,24,40,.4);rotate:-2deg}
.gx-file[data-grabbed]{border-color:var(--gx-accent);box-shadow:0 0 0 2px var(--gx-accent)}
@media (prefers-reduced-motion:reduce){.gx-zone{transition:none}.gx-row[data-dd-ghost],.gx-file[data-dd-ghost],.gx-preview .gx-ghost{rotate:none}}
JavaScript
// 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 class="text-gx-ink"><span class="mb-3 block text-[.72rem] tracking-[.12em] text-gx-muted uppercase">Handle styles</span>
  <p class="mb-2.5 text-[.8rem] text-gx-muted" id="gx-hint1">Drag a row, or focus its handle, press Space and use the arrow keys.</p>
  <div class="grid grid-cols-[repeat(auto-fit,minmax(190px,1fr))] gap-3">
    <section class="rounded-[14px] border border-gx-line bg-white p-3.5 shadow-[0_1px_2px_rgba(16,24,40,.05)]" data-dd aria-labelledby="gx-dots-t"><h3 class="text-[.88rem] font-bold" id="gx-dots-t">Dot grip</h3><p class="mt-0.5 mb-2.5 text-[.78rem] text-gx-muted">The familiar six-dot grip on the left.</p>
      <ul class="grid grid-cols-[minmax(0,1fr)] gap-1.5 *:data-dragging:border-2 *:data-dragging:border-dashed *:data-dragging:border-[#e68bb4] *:data-dragging:bg-gx-tint [&>[data-dragging]>*]:invisible *:data-dd-ghost:-rotate-1 *:data-dd-ghost:shadow-[0_14px_30px_-10px_rgba(16,24,40,.4)] motion-reduce:*:data-dd-ghost:rotate-0 *:data-grabbed:border-gx-accent *:data-grabbed:shadow-[0_0_0_2px_#be185d]" data-dd-list="dots" aria-label="Playlist"><li class="flex min-h-10 cursor-grab items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-1 text-[.86rem] font-semibold select-none" data-dd-item><button class="grid h-[30px] w-7 flex-none cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-gx-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-gx-accent" type="button" data-dd-handle aria-describedby="gx-hint1"><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 truncate" data-dd-name>Northern Lights</span></li><li class="flex min-h-10 cursor-grab items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-1 text-[.86rem] font-semibold select-none" data-dd-item><button class="grid h-[30px] w-7 flex-none cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-gx-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-gx-accent" type="button" data-dd-handle aria-describedby="gx-hint1"><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 truncate" data-dd-name>Paper Planes</span></li><li class="flex min-h-10 cursor-grab items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-1 text-[.86rem] font-semibold select-none" data-dd-item><button class="grid h-[30px] w-7 flex-none cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-gx-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-gx-accent" type="button" data-dd-handle aria-describedby="gx-hint1"><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 truncate" data-dd-name>Slow Tide</span></li></ul>
      <p class="sr-only" aria-live="assertive" aria-atomic="true" data-dd-live></p></section>
    <section class="rounded-[14px] border border-gx-line bg-white p-3.5 shadow-[0_1px_2px_rgba(16,24,40,.05)]" data-dd aria-labelledby="gx-bars-t"><h3 class="text-[.88rem] font-bold" id="gx-bars-t">Bars on the right</h3><p class="mt-0.5 mb-2.5 text-[.78rem] text-gx-muted">Keeps the start of each row for content.</p>
      <ul class="grid grid-cols-[minmax(0,1fr)] gap-1.5 *:data-dragging:border-2 *:data-dragging:border-dashed *:data-dragging:border-[#e68bb4] *:data-dragging:bg-gx-tint [&>[data-dragging]>*]:invisible *:data-dd-ghost:-rotate-1 *:data-dd-ghost:shadow-[0_14px_30px_-10px_rgba(16,24,40,.4)] motion-reduce:*:data-dd-ghost:rotate-0 *:data-grabbed:border-gx-accent *:data-grabbed:shadow-[0_0_0_2px_#be185d]" data-dd-list="bars" aria-label="Checkout steps"><li class="flex min-h-10 cursor-grab items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-1 pl-3 text-[.86rem] font-semibold select-none" data-dd-item><span class="min-w-0 flex-1 truncate" data-dd-name>Shipping</span><button class="grid h-[30px] w-7 flex-none cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-gx-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-gx-accent" type="button" data-dd-handle aria-describedby="gx-hint1"><svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M3 6h14v2H3zM3 12h14v2H3z"/></svg></button></li><li class="flex min-h-10 cursor-grab items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-1 pl-3 text-[.86rem] font-semibold select-none" data-dd-item><span class="min-w-0 flex-1 truncate" data-dd-name>Payment</span><button class="grid h-[30px] w-7 flex-none cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-gx-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-gx-accent" type="button" data-dd-handle aria-describedby="gx-hint1"><svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M3 6h14v2H3zM3 12h14v2H3z"/></svg></button></li><li class="flex min-h-10 cursor-grab items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-1 pl-3 text-[.86rem] font-semibold select-none" data-dd-item><span class="min-w-0 flex-1 truncate" data-dd-name>Review</span><button class="grid h-[30px] w-7 flex-none cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-gx-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-gx-accent" type="button" data-dd-handle aria-describedby="gx-hint1"><svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M3 6h14v2H3zM3 12h14v2H3z"/></svg></button></li></ul>
      <p class="sr-only" aria-live="assertive" aria-atomic="true" data-dd-live></p></section>
    <section class="rounded-[14px] border border-gx-line bg-white p-3.5 shadow-[0_1px_2px_rgba(16,24,40,.05)]" data-dd aria-labelledby="gx-strip-t"><h3 class="text-[.88rem] font-bold" id="gx-strip-t">Edge strip</h3><p class="mt-0.5 mb-2.5 text-[.78rem] text-gx-muted">A full-height strip: a big target for touch.</p>
      <ul class="grid grid-cols-[minmax(0,1fr)] gap-1.5 *:data-dragging:border-2 *:data-dragging:border-dashed *:data-dragging:border-[#e68bb4] *:data-dragging:bg-gx-tint [&>[data-dragging]>*]:invisible *:data-dd-ghost:-rotate-1 *:data-dd-ghost:shadow-[0_14px_30px_-10px_rgba(16,24,40,.4)] motion-reduce:*:data-dd-ghost:rotate-0 *:data-grabbed:border-gx-accent *:data-grabbed:shadow-[0_0_0_2px_#be185d]" data-dd-list="strip" aria-label="Homepage blocks"><li class="flex min-h-10 cursor-grab items-center gap-2 rounded-[10px] border border-gx-line bg-white overflow-hidden pr-2.5 text-[.86rem] font-semibold select-none" data-dd-item><button class="grid w-[22px] flex-none cursor-grab touch-none place-items-center self-stretch border-r border-[#f6cfe0] bg-gx-tint text-gx-accent hover:bg-[#fbe3ee] focus-visible:outline-2 focus-visible:-outline-offset-3 focus-visible:outline-gx-accent" type="button" data-dd-handle aria-describedby="gx-hint1"><svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="8" cy="4" r="1.4"/><circle cx="12" cy="4" r="1.4"/><circle cx="8" cy="8" r="1.4"/><circle cx="12" cy="8" r="1.4"/><circle cx="8" cy="12" r="1.4"/><circle cx="12" cy="12" r="1.4"/><circle cx="8" cy="16" r="1.4"/><circle cx="12" cy="16" r="1.4"/></svg></button><span class="min-w-0 flex-1 truncate" data-dd-name>Hero</span></li><li class="flex min-h-10 cursor-grab items-center gap-2 rounded-[10px] border border-gx-line bg-white overflow-hidden pr-2.5 text-[.86rem] font-semibold select-none" data-dd-item><button class="grid w-[22px] flex-none cursor-grab touch-none place-items-center self-stretch border-r border-[#f6cfe0] bg-gx-tint text-gx-accent hover:bg-[#fbe3ee] focus-visible:outline-2 focus-visible:-outline-offset-3 focus-visible:outline-gx-accent" type="button" data-dd-handle aria-describedby="gx-hint1"><svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="8" cy="4" r="1.4"/><circle cx="12" cy="4" r="1.4"/><circle cx="8" cy="8" r="1.4"/><circle cx="12" cy="8" r="1.4"/><circle cx="8" cy="12" r="1.4"/><circle cx="12" cy="12" r="1.4"/><circle cx="8" cy="16" r="1.4"/><circle cx="12" cy="16" r="1.4"/></svg></button><span class="min-w-0 flex-1 truncate" data-dd-name>Customer logos</span></li><li class="flex min-h-10 cursor-grab items-center gap-2 rounded-[10px] border border-gx-line bg-white overflow-hidden pr-2.5 text-[.86rem] font-semibold select-none" data-dd-item><button class="grid w-[22px] flex-none cursor-grab touch-none place-items-center self-stretch border-r border-[#f6cfe0] bg-gx-tint text-gx-accent hover:bg-[#fbe3ee] focus-visible:outline-2 focus-visible:-outline-offset-3 focus-visible:outline-gx-accent" type="button" data-dd-handle aria-describedby="gx-hint1"><svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><circle cx="8" cy="4" r="1.4"/><circle cx="12" cy="4" r="1.4"/><circle cx="8" cy="8" r="1.4"/><circle cx="12" cy="8" r="1.4"/><circle cx="8" cy="12" r="1.4"/><circle cx="12" cy="12" r="1.4"/><circle cx="8" cy="16" r="1.4"/><circle cx="12" cy="16" r="1.4"/></svg></button><span class="min-w-0 flex-1 truncate" data-dd-name>Pricing</span></li></ul>
      <p class="sr-only" aria-live="assertive" aria-atomic="true" data-dd-live></p></section>
    <section class="rounded-[14px] border border-gx-line bg-white p-3.5 shadow-[0_1px_2px_rgba(16,24,40,.05)]" data-dd data-dd-handle-only aria-labelledby="gx-move-t"><h3 class="text-[.88rem] font-bold" id="gx-move-t">Handle only</h3><p class="mt-0.5 mb-2.5 text-[.78rem] text-gx-muted">Rows ignore the pointer, so text stays selectable.</p>
      <ul class="grid grid-cols-[minmax(0,1fr)] gap-1.5 *:data-dragging:border-2 *:data-dragging:border-dashed *:data-dragging:border-[#e68bb4] *:data-dragging:bg-gx-tint [&>[data-dragging]>*]:invisible *:data-dd-ghost:-rotate-1 *:data-dd-ghost:shadow-[0_14px_30px_-10px_rgba(16,24,40,.4)] motion-reduce:*:data-dd-ghost:rotate-0 *:data-grabbed:border-gx-accent *:data-grabbed:shadow-[0_0_0_2px_#be185d]" data-dd-list="move" aria-label="Signature lines"><li class="flex min-h-10 cursor-auto items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-1 text-[.86rem] font-semibold select-text" data-dd-item><button class="grid h-[30px] w-7 flex-none cursor-move touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-gx-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-gx-accent border border-[#d0d5dd]" type="button" data-dd-handle aria-describedby="gx-hint1"><svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M10 2 7 5h2v4H5V7L2 10l3 3v-2h4v4H7l3 3 3-3h-2v-4h4v2l3-3-3-3v2h-4V5h2z"/></svg></button><span class="min-w-0 flex-1 truncate" data-dd-name>Mara Jensen</span></li><li class="flex min-h-10 cursor-auto items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-1 text-[.86rem] font-semibold select-text" data-dd-item><button class="grid h-[30px] w-7 flex-none cursor-move touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-gx-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-gx-accent border border-[#d0d5dd]" type="button" data-dd-handle aria-describedby="gx-hint1"><svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M10 2 7 5h2v4H5V7L2 10l3 3v-2h4v4H7l3 3 3-3h-2v-4h4v2l3-3-3-3v2h-4V5h2z"/></svg></button><span class="min-w-0 flex-1 truncate" data-dd-name>Head of Design</span></li><li class="flex min-h-10 cursor-auto items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-1 text-[.86rem] font-semibold select-text" data-dd-item><button class="grid h-[30px] w-7 flex-none cursor-move touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-gx-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-gx-accent border border-[#d0d5dd]" type="button" data-dd-handle aria-describedby="gx-hint1"><svg class="size-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M10 2 7 5h2v4H5V7L2 10l3 3v-2h4v4H7l3 3 3-3h-2v-4h4v2l3-3-3-3v2h-4V5h2z"/></svg></button><span class="min-w-0 flex-1 truncate" data-dd-name>[email protected]</span></li></ul>
      <p class="sr-only" aria-live="assertive" aria-atomic="true" data-dd-live></p></section>
  </div></div>

<div class="text-gx-ink"><span class="mb-3 block text-[.72rem] tracking-[.12em] text-gx-muted uppercase">Placeholder styles</span>
  <div class="grid grid-cols-[repeat(auto-fit,minmax(220px,1fr))] gap-3">
    <section class="rounded-[14px] border border-gx-line bg-white p-3.5 shadow-[0_1px_2px_rgba(16,24,40,.05)]" data-dd aria-labelledby="gx-slot-t"><h3 class="text-[.88rem] font-bold" id="gx-slot-t">Dashed slot</h3><p class="mt-0.5 mb-2.5 text-[.78rem] text-gx-muted">Holds the exact size of the item being moved.</p>
      <span class="mb-1.5 inline-block text-[.66rem] font-extrabold tracking-[.06em] text-gx-muted uppercase" aria-hidden="true">Mid-drag</span><div class="relative mb-2.5 grid gap-1.5 rounded-[10px] border border-gx-line bg-[#f8f9fb] px-2.5 pt-2.5 pb-[54px] *:data-dragging:border-2 *:data-dragging:border-dashed *:data-dragging:border-[#e68bb4] *:data-dragging:bg-gx-tint [&>[data-dragging]>*]:invisible" aria-hidden="true"><div class="flex min-h-10 cursor-default items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-3 text-[.86rem] font-semibold select-none"><span>Intro video</span></div><div class="flex min-h-10 cursor-default items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-3 text-[.86rem] font-semibold select-none" data-dragging><span>Lesson 1: Setup</span></div><div class="flex min-h-10 cursor-default items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-3 text-[.86rem] font-semibold select-none"><span>Lesson 2: Layout</span></div><div class="flex min-h-10 cursor-default items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-3 text-[.86rem] font-semibold select-none absolute right-[-6px] bottom-2.5 left-[34px] -rotate-[1.5deg] shadow-[0_14px_30px_-10px_rgba(16,24,40,.4)] motion-reduce:rotate-0"><span>Lesson 1: Setup</span></div></div><span class="mb-1.5 inline-block text-[.66rem] font-extrabold tracking-[.06em] text-gx-muted uppercase" aria-hidden="true">Try it</span>
      <ul class="grid grid-cols-[minmax(0,1fr)] gap-1.5 *:data-dragging:border-2 *:data-dragging:border-dashed *:data-dragging:border-[#e68bb4] *:data-dragging:bg-gx-tint [&>[data-dragging]>*]:invisible *:data-dd-ghost:-rotate-1 *:data-dd-ghost:shadow-[0_14px_30px_-10px_rgba(16,24,40,.4)] motion-reduce:*:data-dd-ghost:rotate-0 *:data-grabbed:border-gx-accent *:data-grabbed:shadow-[0_0_0_2px_#be185d]" data-dd-list="slot" aria-label="Course outline, dashed slot"><li class="flex min-h-10 cursor-grab items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-1 text-[.86rem] font-semibold select-none" data-dd-item><button class="grid h-[30px] w-7 flex-none cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-gx-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-gx-accent" type="button" data-dd-handle aria-describedby="gx-hint1"><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 truncate" data-dd-name>Intro video</span></li><li class="flex min-h-10 cursor-grab items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-1 text-[.86rem] font-semibold select-none" data-dd-item><button class="grid h-[30px] w-7 flex-none cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-gx-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-gx-accent" type="button" data-dd-handle aria-describedby="gx-hint1"><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 truncate" data-dd-name>Lesson 1: Setup</span></li><li class="flex min-h-10 cursor-grab items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-1 text-[.86rem] font-semibold select-none" data-dd-item><button class="grid h-[30px] w-7 flex-none cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-gx-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-gx-accent" type="button" data-dd-handle aria-describedby="gx-hint1"><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 truncate" data-dd-name>Lesson 2: Layout</span></li></ul>
      <p class="sr-only" aria-live="assertive" aria-atomic="true" data-dd-live></p></section>
    <section class="rounded-[14px] border border-gx-line bg-white p-3.5 shadow-[0_1px_2px_rgba(16,24,40,.05)]" data-dd aria-labelledby="gx-line-t"><h3 class="text-[.88rem] font-bold" id="gx-line-t">Insertion line</h3><p class="mt-0.5 mb-2.5 text-[.78rem] text-gx-muted">Collapses to a line, so the list barely shifts.</p>
      <span class="mb-1.5 inline-block text-[.66rem] font-extrabold tracking-[.06em] text-gx-muted uppercase" aria-hidden="true">Mid-drag</span><div class="relative mb-2.5 grid gap-1.5 rounded-[10px] border border-gx-line bg-[#f8f9fb] px-2.5 pt-2.5 pb-[54px] *:data-dragging:relative *:data-dragging:my-[3px] *:data-dragging:h-1 *:data-dragging:min-h-0 *:data-dragging:overflow-visible *:data-dragging:rounded-sm *:data-dragging:border-0 *:data-dragging:bg-gx-accent *:data-dragging:p-0 [&>[data-dragging]>*]:hidden *:data-dragging:before:absolute *:data-dragging:before:-top-1 *:data-dragging:before:-left-0.5 *:data-dragging:before:size-3 *:data-dragging:before:rounded-full *:data-dragging:before:border-3 *:data-dragging:before:border-gx-accent *:data-dragging:before:bg-white *:data-dragging:before:content-['']" aria-hidden="true"><div class="flex min-h-10 cursor-default items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-3 text-[.86rem] font-semibold select-none"><span>Intro video</span></div><div class="flex min-h-10 cursor-default items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-3 text-[.86rem] font-semibold select-none" data-dragging><span>Lesson 1: Setup</span></div><div class="flex min-h-10 cursor-default items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-3 text-[.86rem] font-semibold select-none"><span>Lesson 2: Layout</span></div><div class="flex min-h-10 cursor-default items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-3 text-[.86rem] font-semibold select-none absolute right-[-6px] bottom-2.5 left-[34px] -rotate-[1.5deg] shadow-[0_14px_30px_-10px_rgba(16,24,40,.4)] motion-reduce:rotate-0"><span>Lesson 1: Setup</span></div></div><span class="mb-1.5 inline-block text-[.66rem] font-extrabold tracking-[.06em] text-gx-muted uppercase" aria-hidden="true">Try it</span>
      <ul class="grid grid-cols-[minmax(0,1fr)] gap-1.5 *:data-dragging:relative *:data-dragging:my-[3px] *:data-dragging:h-1 *:data-dragging:min-h-0 *:data-dragging:overflow-visible *:data-dragging:rounded-sm *:data-dragging:border-0 *:data-dragging:bg-gx-accent *:data-dragging:p-0 [&>[data-dragging]>*]:hidden *:data-dragging:before:absolute *:data-dragging:before:-top-1 *:data-dragging:before:-left-0.5 *:data-dragging:before:size-3 *:data-dragging:before:rounded-full *:data-dragging:before:border-3 *:data-dragging:before:border-gx-accent *:data-dragging:before:bg-white *:data-dragging:before:content-[''] *:data-dd-ghost:-rotate-1 *:data-dd-ghost:shadow-[0_14px_30px_-10px_rgba(16,24,40,.4)] motion-reduce:*:data-dd-ghost:rotate-0 *:data-grabbed:border-gx-accent *:data-grabbed:shadow-[0_0_0_2px_#be185d]" data-dd-list="line" aria-label="Course outline, insertion line"><li class="flex min-h-10 cursor-grab items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-1 text-[.86rem] font-semibold select-none" data-dd-item><button class="grid h-[30px] w-7 flex-none cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-gx-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-gx-accent" type="button" data-dd-handle aria-describedby="gx-hint1"><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 truncate" data-dd-name>Intro video</span></li><li class="flex min-h-10 cursor-grab items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-1 text-[.86rem] font-semibold select-none" data-dd-item><button class="grid h-[30px] w-7 flex-none cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-gx-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-gx-accent" type="button" data-dd-handle aria-describedby="gx-hint1"><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 truncate" data-dd-name>Lesson 1: Setup</span></li><li class="flex min-h-10 cursor-grab items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-1 text-[.86rem] font-semibold select-none" data-dd-item><button class="grid h-[30px] w-7 flex-none cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-gx-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-gx-accent" type="button" data-dd-handle aria-describedby="gx-hint1"><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 truncate" data-dd-name>Lesson 2: Layout</span></li></ul>
      <p class="sr-only" aria-live="assertive" aria-atomic="true" data-dd-live></p></section>
    <section class="rounded-[14px] border border-gx-line bg-white p-3.5 shadow-[0_1px_2px_rgba(16,24,40,.05)]" data-dd aria-labelledby="gx-fade-t"><h3 class="text-[.88rem] font-bold" id="gx-fade-t">Faded original</h3><p class="mt-0.5 mb-2.5 text-[.78rem] text-gx-muted">Leaves the item in place at low opacity.</p>
      <span class="mb-1.5 inline-block text-[.66rem] font-extrabold tracking-[.06em] text-gx-muted uppercase" aria-hidden="true">Mid-drag</span><div class="relative mb-2.5 grid gap-1.5 rounded-[10px] border border-gx-line bg-[#f8f9fb] px-2.5 pt-2.5 pb-[54px] *:data-dragging:border-dashed *:data-dragging:border-[#98a2b3] *:data-dragging:opacity-60" aria-hidden="true"><div class="flex min-h-10 cursor-default items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-3 text-[.86rem] font-semibold select-none"><span>Intro video</span></div><div class="flex min-h-10 cursor-default items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-3 text-[.86rem] font-semibold select-none" data-dragging><span>Lesson 1: Setup</span></div><div class="flex min-h-10 cursor-default items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-3 text-[.86rem] font-semibold select-none"><span>Lesson 2: Layout</span></div><div class="flex min-h-10 cursor-default items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-3 text-[.86rem] font-semibold select-none absolute right-[-6px] bottom-2.5 left-[34px] -rotate-[1.5deg] shadow-[0_14px_30px_-10px_rgba(16,24,40,.4)] motion-reduce:rotate-0"><span>Lesson 1: Setup</span></div></div><span class="mb-1.5 inline-block text-[.66rem] font-extrabold tracking-[.06em] text-gx-muted uppercase" aria-hidden="true">Try it</span>
      <ul class="grid grid-cols-[minmax(0,1fr)] gap-1.5 *:data-dragging:border-dashed *:data-dragging:border-[#98a2b3] *:data-dragging:opacity-60 *:data-dd-ghost:-rotate-1 *:data-dd-ghost:shadow-[0_14px_30px_-10px_rgba(16,24,40,.4)] motion-reduce:*:data-dd-ghost:rotate-0 *:data-grabbed:border-gx-accent *:data-grabbed:shadow-[0_0_0_2px_#be185d]" data-dd-list="fade" aria-label="Course outline, faded original"><li class="flex min-h-10 cursor-grab items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-1 text-[.86rem] font-semibold select-none" data-dd-item><button class="grid h-[30px] w-7 flex-none cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-gx-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-gx-accent" type="button" data-dd-handle aria-describedby="gx-hint1"><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 truncate" data-dd-name>Intro video</span></li><li class="flex min-h-10 cursor-grab items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-1 text-[.86rem] font-semibold select-none" data-dd-item><button class="grid h-[30px] w-7 flex-none cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-gx-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-gx-accent" type="button" data-dd-handle aria-describedby="gx-hint1"><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 truncate" data-dd-name>Lesson 1: Setup</span></li><li class="flex min-h-10 cursor-grab items-center gap-2 rounded-[10px] border border-gx-line bg-white py-1 pr-2.5 pl-1 text-[.86rem] font-semibold select-none" data-dd-item><button class="grid h-[30px] w-7 flex-none cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-gx-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-gx-accent" type="button" data-dd-handle aria-describedby="gx-hint1"><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 truncate" data-dd-name>Lesson 2: Layout</span></li></ul>
      <p class="sr-only" aria-live="assertive" aria-atomic="true" data-dd-live></p></section>
  </div></div>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-gx-muted uppercase">Drop zone states</span>
<section class="group/gx text-gx-ink rounded-[14px] border border-gx-line bg-white p-3.5 shadow-[0_1px_2px_rgba(16,24,40,.05)]" data-dd="wrap" aria-labelledby="gx-z-t">
  <h3 id="gx-z-t" class="sr-only">Drop zone states</h3>
  <div class="mb-3.5 grid grid-cols-[repeat(auto-fit,minmax(150px,1fr))] gap-2.5">
    <div class="grid min-h-[86px] content-start gap-0.5 rounded-xl border px-3 py-2.5 text-[.78rem] text-gx-muted outline-2 -outline-offset-2 outline-dashed transition-colors motion-reduce:transition-none border-gx-line bg-[#f8f9fb] outline-transparent"><b class="text-[.84rem] text-gx-ink">Idle</b>No drag in progress.</div>
    <div class="grid min-h-[86px] content-start gap-0.5 rounded-xl border px-3 py-2.5 text-[.78rem] text-gx-muted outline-2 -outline-offset-2 outline-dashed transition-colors motion-reduce:transition-none border-transparent bg-[#f8f9fb] outline-[#98a2b3]"><b class="text-[.84rem] text-gx-ink">Ready</b>A drag started: every valid target shows a dashed outline.</div>
    <div class="grid min-h-[86px] content-start gap-0.5 rounded-xl border px-3 py-2.5 text-[.78rem] text-gx-muted outline-2 -outline-offset-2 outline-dashed transition-colors motion-reduce:transition-none border-transparent bg-gx-tint outline-solid outline-gx-accent"><b class="text-[.84rem] text-gx-ink">Over</b>The item is above this target and will land here.</div>
    <div class="grid min-h-[86px] content-start gap-0.5 rounded-xl border px-3 py-2.5 text-[.78rem] text-gx-muted outline-2 -outline-offset-2 outline-dashed transition-colors motion-reduce:transition-none border-transparent bg-[#fef3f2] outline-solid outline-gx-danger [&_b]:text-gx-danger"><b class="text-[.84rem] text-gx-ink">Full</b>The target is at its limit and refuses the item.</div>
  </div>
  <p class="mb-2.5 text-[.8rem] text-gx-muted" id="gx-hint3">Drag a file into a folder, or focus its handle, press Space and use Left and Right. Shared holds 2 files.</p>
  <div class="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-2.5">
    <div class="grid min-h-[86px] content-start gap-0.5 rounded-xl border px-3 py-2.5 text-[.78rem] text-gx-muted outline-2 -outline-offset-2 outline-dashed transition-colors motion-reduce:transition-none border-gx-line bg-[#f8f9fb] outline-transparent group-data-[dd-active]/gx:border-transparent group-data-[dd-active]/gx:outline-[#98a2b3] has-[>[data-over]]:border-transparent has-[>[data-over]]:bg-gx-tint has-[>[data-over]]:outline-solid has-[>[data-over]]:outline-gx-accent! [&:has(>[data-over][data-full]):not(:has([data-dragging],[data-grabbed]))]:bg-[#fef3f2] [&:has(>[data-over][data-full]):not(:has([data-dragging],[data-grabbed]))]:outline-gx-danger!" data-dd-zone><div class="mb-1.5 flex justify-between gap-2 text-[.84rem] font-bold text-gx-ink">Inbox<span class="text-[.75rem] font-normal text-gx-muted tabular-nums"><span data-dd-count="gx-inbox">3</span></span></div><ul class="flex min-h-10 flex-wrap content-start gap-1.5 data-empty:after:self-center data-empty:after:text-[.78rem] data-empty:after:text-gx-muted data-empty:after:content-['Empty_folder'] *:data-dragging:border-2 *:data-dragging:border-dashed *:data-dragging:border-[#e68bb4] *:data-dragging:bg-gx-tint [&>[data-dragging]>*]:invisible *:data-dd-ghost:-rotate-2 *:data-dd-ghost:shadow-[0_14px_30px_-10px_rgba(16,24,40,.4)] motion-reduce:*:data-dd-ghost:rotate-0 *:data-grabbed:border-gx-accent *:data-grabbed:shadow-[0_0_0_2px_#be185d]" id="gx-inbox" data-dd-list="files" aria-label="Inbox"><li class="flex cursor-grab items-center gap-1 rounded-lg border border-[#d0d5dd] bg-white py-0.5 pr-2.5 pl-0.5 text-[.8rem] font-semibold text-gx-ink select-none" data-dd-item><button class="grid h-[30px] w-7 flex-none cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-gx-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-gx-accent" type="button" data-dd-handle aria-describedby="gx-hint3"><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><svg class="size-4 text-gx-accent" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M5 2.5h6.5L15 6v11.5H5zM11 2.5V6.5h4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg><span data-dd-name>brief-v3.pdf</span></li><li class="flex cursor-grab items-center gap-1 rounded-lg border border-[#d0d5dd] bg-white py-0.5 pr-2.5 pl-0.5 text-[.8rem] font-semibold text-gx-ink select-none" data-dd-item><button class="grid h-[30px] w-7 flex-none cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-gx-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-gx-accent" type="button" data-dd-handle aria-describedby="gx-hint3"><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><svg class="size-4 text-gx-accent" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M5 2.5h6.5L15 6v11.5H5zM11 2.5V6.5h4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg><span data-dd-name>logo.svg</span></li><li class="flex cursor-grab items-center gap-1 rounded-lg border border-[#d0d5dd] bg-white py-0.5 pr-2.5 pl-0.5 text-[.8rem] font-semibold text-gx-ink select-none" data-dd-item><button class="grid h-[30px] w-7 flex-none cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-gx-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-gx-accent" type="button" data-dd-handle aria-describedby="gx-hint3"><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><svg class="size-4 text-gx-accent" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M5 2.5h6.5L15 6v11.5H5zM11 2.5V6.5h4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg><span data-dd-name>meeting-notes.md</span></li></ul></div>
    <div class="grid min-h-[86px] content-start gap-0.5 rounded-xl border px-3 py-2.5 text-[.78rem] text-gx-muted outline-2 -outline-offset-2 outline-dashed transition-colors motion-reduce:transition-none border-gx-line bg-[#f8f9fb] outline-transparent group-data-[dd-active]/gx:border-transparent group-data-[dd-active]/gx:outline-[#98a2b3] has-[>[data-over]]:border-transparent has-[>[data-over]]:bg-gx-tint has-[>[data-over]]:outline-solid has-[>[data-over]]:outline-gx-accent! [&:has(>[data-over][data-full]):not(:has([data-dragging],[data-grabbed]))]:bg-[#fef3f2] [&:has(>[data-over][data-full]):not(:has([data-dragging],[data-grabbed]))]:outline-gx-danger!" data-dd-zone><div class="mb-1.5 flex justify-between gap-2 text-[.84rem] font-bold text-gx-ink">Archive<span class="text-[.75rem] font-normal text-gx-muted tabular-nums"><span data-dd-count="gx-archive">0</span></span></div><ul class="flex min-h-10 flex-wrap content-start gap-1.5 data-empty:after:self-center data-empty:after:text-[.78rem] data-empty:after:text-gx-muted data-empty:after:content-['Empty_folder'] *:data-dragging:border-2 *:data-dragging:border-dashed *:data-dragging:border-[#e68bb4] *:data-dragging:bg-gx-tint [&>[data-dragging]>*]:invisible *:data-dd-ghost:-rotate-2 *:data-dd-ghost:shadow-[0_14px_30px_-10px_rgba(16,24,40,.4)] motion-reduce:*:data-dd-ghost:rotate-0 *:data-grabbed:border-gx-accent *:data-grabbed:shadow-[0_0_0_2px_#be185d]" id="gx-archive" data-dd-list="files" aria-label="Archive"></ul></div>
    <div class="grid min-h-[86px] content-start gap-0.5 rounded-xl border px-3 py-2.5 text-[.78rem] text-gx-muted outline-2 -outline-offset-2 outline-dashed transition-colors motion-reduce:transition-none border-gx-line bg-[#f8f9fb] outline-transparent group-data-[dd-active]/gx:border-transparent group-data-[dd-active]/gx:outline-[#98a2b3] has-[>[data-over]]:border-transparent has-[>[data-over]]:bg-gx-tint has-[>[data-over]]:outline-solid has-[>[data-over]]:outline-gx-accent! [&:has(>[data-over][data-full]):not(:has([data-dragging],[data-grabbed]))]:bg-[#fef3f2] [&:has(>[data-over][data-full]):not(:has([data-dragging],[data-grabbed]))]:outline-gx-danger!" data-dd-zone><div class="mb-1.5 flex justify-between gap-2 text-[.84rem] font-bold text-gx-ink">Shared<span class="text-[.75rem] font-normal text-gx-muted tabular-nums"><span data-dd-count="gx-shared">1</span> / 2</span></div><ul class="flex min-h-10 flex-wrap content-start gap-1.5 data-empty:after:self-center data-empty:after:text-[.78rem] data-empty:after:text-gx-muted data-empty:after:content-['Empty_folder'] *:data-dragging:border-2 *:data-dragging:border-dashed *:data-dragging:border-[#e68bb4] *:data-dragging:bg-gx-tint [&>[data-dragging]>*]:invisible *:data-dd-ghost:-rotate-2 *:data-dd-ghost:shadow-[0_14px_30px_-10px_rgba(16,24,40,.4)] motion-reduce:*:data-dd-ghost:rotate-0 *:data-grabbed:border-gx-accent *:data-grabbed:shadow-[0_0_0_2px_#be185d]" id="gx-shared" data-dd-list="files" data-dd-max="2" aria-label="Shared"><li class="flex cursor-grab items-center gap-1 rounded-lg border border-[#d0d5dd] bg-white py-0.5 pr-2.5 pl-0.5 text-[.8rem] font-semibold text-gx-ink select-none" data-dd-item><button class="grid h-[30px] w-7 flex-none cursor-grab touch-none place-items-center rounded-[7px] text-[#667085] hover:bg-[#f2f4f7] hover:text-gx-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-gx-accent" type="button" data-dd-handle aria-describedby="gx-hint3"><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><svg class="size-4 text-gx-accent" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" focusable="false"><path d="M5 2.5h6.5L15 6v11.5H5zM11 2.5V6.5h4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg><span data-dd-name>budget.xlsx</span></li></ul></div>
  </div>
  <p class="sr-only" aria-live="assertive" aria-atomic="true" data-dd-live></p>
</section></div>
CSS
@theme {
  --color-gx-accent: #be185d;
  --color-gx-tint: #fdf0f6;
  --color-gx-line: #e4e7ec;
  --color-gx-muted: #5d6679;
  --color-gx-ink: #101828;
  --color-gx-danger: #b42318;
}
JavaScript
// 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

A reference set of the small details that make drag and drop feel right, every one of them working. Handle styles shows four lists with a six-dot grip on the left, bars on the right, a full-height edge strip that makes a big target for touch, and a handle-only list where rows ignore the pointer so their text can still be selected. Placeholder styles shows the same course outline three ways while an item is being moved: a dashed slot the size of the item, an insertion line that barely shifts the list, and the original left in place, faded, with a frozen mid-drag preview above each live list. Drop zone states sets out idle, ready, over and full side by side, then puts them to work in three folders, where Shared takes only two files. The script sets plain data attributes, data-dd-active on the component, data-over and data-full on targets, data-dragging on the placeholder and data-grabbed in keyboard mode, so every look here is CSS you can lift into your own lists.

What’s included

  • Four handle styles, including a handle-only mode
  • Three placeholder styles with mid-drag previews
  • Idle, ready, over and full drop states
  • Folder targets with a two-file limit
  • Every state driven by data attributes and CSS

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 "logo.svg moved to Archive, position 1 of 2.". 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 --gx-accent and --gx-tint on .gx, set data-ph to slot, line or fade on a list root for the placeholder style, and add data-dd-handle-only to a root so only the handle starts a drag. Bootstrap and Tailwind use the same attributes.