24 CSS Tags & Chips17 / 24

CSS + JSMIT licensed

Draggable Tag List Reordering UI

Reorderable tags with no library — the HTML5 drag API for pointers, plus the part every tutorial skips: full keyboard reordering. Tab to a tag, press Space to lift it (the tag visibly floats), arrow keys walk it through the list, Space drops, Escape cancels — with every move announced through aria-live. Pointer drags compute the insertion point from cursor position, the lifted tag tilts 4° like a picked-up card, and neighbors shuffle aside with real transitions via a 10-line FLIP.

Published Updated

Live Demo
Try it

The code

<section class="ctc-17" aria-label="Draggable tag reordering demo">
  <div class="ctc-17__stage">
    <div class="ctc-17__wrap">
      <header class="ctc-17__head">
        <p class="ctc-17__kicker">Drag to reorder · pointer + keyboard</p>
        <h1>Grab one. Mouse or Space bar.</h1>
        <p class="ctc-17__sub">Drag with the pointer, or Tab to a tag and press <kbd>Space</kbd> to lift, <kbd>←</kbd><kbd>→</kbd> to move, <kbd>Space</kbd> to drop, <kbd>Esc</kbd> to cancel.</p>
      </header>
      <div class="ctc-17__card">
        <p class="ctc-17__lab" id="ctc17-lab">Interview pipeline stages — priority order</p>
        <div class="ctc-17__list" role="list" aria-labelledby="ctc17-lab">
          <span class="ctc-17__tag" role="listitem" draggable="true" tabindex="0" style="--h:255"><i aria-hidden="true">⠿</i>screening</span>
          <span class="ctc-17__tag" role="listitem" draggable="true" tabindex="0" style="--h:200"><i aria-hidden="true">⠿</i>portfolio</span>
          <span class="ctc-17__tag" role="listitem" draggable="true" tabindex="0" style="--h:150"><i aria-hidden="true">⠿</i>pairing</span>
          <span class="ctc-17__tag" role="listitem" draggable="true" tabindex="0" style="--h:85"><i aria-hidden="true">⠿</i>system design</span>
          <span class="ctc-17__tag" role="listitem" draggable="true" tabindex="0" style="--h:40"><i aria-hidden="true">⠿</i>values</span>
          <span class="ctc-17__tag" role="listitem" draggable="true" tabindex="0" style="--h:320"><i aria-hidden="true">⠿</i>offer</span>
        </div>
        <p class="ctc-17__order">Current order: <output id="ctc17-out"></output></p>
        <p class="ctc-17__sr" aria-live="polite" id="ctc17-live"></p>
      </div>
      <ul class="ctc-17__why" aria-label="What makes this shippable">
        <li><b>Insertion by midpoint</b>dragover finds the first chip whose center is past the cursor — no index math.</li>
        <li><b>10-line FLIP</b>neighbors slide, not teleport: rect before, move, transform back to zero.</li>
        <li><b>Keyboard = first class</b>grab, walk, drop, cancel — every move narrated via <code>aria-live</code>.</li>
      </ul>
    </div>
  </div>
</section>
.ctc-17,
.ctc-17 *,
.ctc-17 *::before,
.ctc-17 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.ctc-17 {
  --bg: oklch(0.96 0.01 260);
  --card: oklch(0.995 0.002 260);
  --ink: oklch(0.25 0.025 265);
  --mut: oklch(0.5 0.02 265);
  --line: oklch(0.88 0.012 260);
  --acc: oklch(0.55 0.18 262);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
  container-type: inline-size;
}

.ctc-17__stage {
  width: 100%;
  height: 100vh;
  height: 100svh;
  overflow-y: auto;
  display: grid;
  place-items: center;
  padding: clamp(16px,4cqi,48px);
  background: radial-gradient(70cqi 55cqi at 10% -10%,color-mix(in oklch,var(--acc) 13%,transparent),transparent 62%),var(--bg);
  container: ctc17/inline-size;
}

.ctc-17__wrap {
  width: min(640px,100%);
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.ctc-17__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.ctc-17__kicker {
  font-size: 11.5px;
  font-weight: 800;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--acc);
}

.ctc-17__head h1 {
  font-size: clamp(24px,3.8cqi,38px);
  font-weight: 800;
  letter-spacing: -.03em;
  margin: 8px 0 8px;
}

.ctc-17__sub {
  font-size: 13px;
  line-height: 1.6;
  color: var(--mut);
}

.ctc-17__sub kbd {
  font-family: ui-monospace,Menlo,monospace;
  font-size: .85em;
  font-weight: 700;
  background: oklch(1 0 0);
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: 5px;
  padding: 1px 6px;
}

.ctc-17__card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 18px;
  padding: 18px;
  box-shadow: 0 20px 50px -30px oklch(0.3 0.05 265/.5);
}

.ctc-17__lab {
  font-size: 11.5px;
  font-weight: 800;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--mut);
  margin-bottom: 13px;
}

.ctc-17__list {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  min-height: 48px;
}

.ctc-17__tag {
  --c: oklch(0.6 0.14 var(--h,255));
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 0 17px 0 11px;
  border-radius: 12px;
  border: 1.5px solid color-mix(in oklch,var(--c) 30%,transparent);
  background: color-mix(in oklch,var(--c) 11%,oklch(1 0 0));
  color: color-mix(in oklch,var(--c) 72%,var(--ink));
  font-size: 13.5px;
  font-weight: 700;
  cursor: grab;
  user-select: none;
  transition: scale .2s,rotate .2s,box-shadow .2s,opacity .2s,border-color .2s;
}

.ctc-17__tag i {
  font-style: normal;
  font-size: 15px;
  opacity: .5;
  letter-spacing: -2px;
}

.ctc-17__tag:hover {
  border-color: color-mix(in oklch,var(--c) 55%,transparent);
  box-shadow: 0 6px 14px -8px var(--c);
}

.ctc-17__tag:focus-visible {
  outline: 2px solid var(--c);
  outline-offset: 2px;
}

.ctc-17__tag.is-drag {
  opacity: .35;
  scale: .97;
  cursor: grabbing;
}

.ctc-17__tag.is-grab {
  scale: 1.06;
  rotate: 4deg;
  box-shadow: 0 16px 30px -12px var(--c);
  border-color: var(--c);
  background: color-mix(in oklch,var(--c) 20%,oklch(1 0 0));
  z-index: 2;
}

.ctc-17__order {
  margin-top: 14px;
  padding-top: 13px;
  border-top: 1px dashed var(--line);
  font-size: 12.5px;
  color: var(--mut);
}

.ctc-17__order output {
  font-family: ui-monospace,Menlo,monospace;
  font-size: 12px;
  font-weight: 600;
  color: var(--ink);
}

.ctc-17__why {
  display: grid;
  grid-template-columns: repeat(auto-fit,minmax(170px,1fr));
  gap: 10px;
  list-style: none;
}

.ctc-17__why li {
  background: oklch(1 0 0/.65);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 13px 15px;
  font-size: 12px;
  line-height: 1.55;
  color: var(--mut);
}

.ctc-17__why b {
  display: block;
  font-size: 12.5px;
  color: var(--ink);
  margin-bottom: 3px;
}

.ctc-17__why code {
  font-family: ui-monospace,Menlo,monospace;
  font-size: .9em;
  background: oklch(0.93 0.008 260);
  padding: 1px 4px;
  border-radius: 4px;
}

@container ctc17 (width < 480px) {
  .ctc-17__why {
    grid-template-columns: 1fr;
  }
}

@media (prefers-reduced-motion: reduce) {
  .ctc-17 * {
    transition-duration: .01ms !important;
  }

  .ctc-17__tag.is-grab {
    rotate: 0deg;
  }
}
(() => {
  const root = document.querySelector('.ctc-17');
  if (!root || root.dataset.init) return;
  root.dataset.init = '1';
  const list = root.querySelector('.ctc-17__list');
  const live = root.querySelector('#ctc17-live');
  const out = root.querySelector('#ctc17-out');
  let dragging = null, grabbed = null, restore = null;
  const tags = () => [...list.querySelectorAll('.ctc-17__tag')];
  const names = () => tags().map((t) => t.textContent.replace('⠿', '').trim());
  const order = () => { out.textContent = names().join(' → '); };
  const pos = (el) => tags().indexOf(el) + 1;
  const say = (msg) => { live.textContent = msg; };
  const flip = (move) => {
    const before = new Map(tags().map((t) => [t, t.getBoundingClientRect()]));
    move();
    tags().forEach((t) => {
      const b = before.get(t); if (!b) return;
      const a = t.getBoundingClientRect();
      const dx = b.left - a.left, dy = b.top - a.top;
      if (!dx && !dy) return;
      t.animate([{ translate: dx + 'px ' + dy + 'px' }, { translate: '0 0' }], { duration: 220, easing: 'cubic-bezier(.2,.8,.2,1)' });
    });
    order();
  };
  list.addEventListener('dragstart', (e) => {
    dragging = e.target.closest('.ctc-17__tag');
    requestAnimationFrame(() => dragging && dragging.classList.add('is-drag'));
  });
  list.addEventListener('dragover', (e) => {
    e.preventDefault();
    if (!dragging) return;
    const after = tags().filter((t) => t !== dragging).find((t) => {
      const r = t.getBoundingClientRect();
      return e.clientY < r.top || (e.clientY <= r.bottom && e.clientX < r.left + r.width / 2);
    });
    if (after !== dragging.nextSibling) flip(() => (after ? list.insertBefore(dragging, after) : list.append(dragging)));
  });
  list.addEventListener('dragend', () => {
    if (!dragging) return;
    dragging.classList.remove('is-drag');
    say(dragging.textContent.replace('⠿', '').trim() + ' dropped at position ' + pos(dragging) + ' of ' + tags().length + '.');
    dragging = null; order();
  });
  list.addEventListener('keydown', (e) => {
    const tag = e.target.closest('.ctc-17__tag');
    if (!tag) return;
    const name = tag.textContent.replace('⠿', '').trim();
    if (e.key === ' ' || e.key === 'Enter') {
      e.preventDefault();
      if (grabbed === tag) {
        grabbed = null; restore = null; tag.classList.remove('is-grab'); tag.setAttribute('aria-pressed', 'false');
        say(name + ' dropped at position ' + pos(tag) + ' of ' + tags().length + '.');
      } else {
        if (grabbed) { grabbed.classList.remove('is-grab'); }
        grabbed = tag; restore = tags();
        tag.classList.add('is-grab'); tag.setAttribute('aria-pressed', 'true');
        say(name + ' grabbed. Position ' + pos(tag) + ' of ' + tags().length + '. Use arrow keys to move.');
      }
    } else if (grabbed === tag && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) {
      e.preventDefault();
      const sib = e.key === 'ArrowLeft' ? tag.previousElementSibling : tag.nextElementSibling;
      if (sib) {
        flip(() => (e.key === 'ArrowLeft' ? list.insertBefore(tag, sib) : list.insertBefore(sib, tag)));
        tag.focus();
        say(name + ' moved to position ' + pos(tag) + ' of ' + tags().length + '.');
      }
    } else if (grabbed === tag && e.key === 'Escape') {
      flip(() => restore.forEach((t) => list.append(t)));
      grabbed.classList.remove('is-grab'); grabbed.setAttribute('aria-pressed', 'false');
      say('Reorder cancelled. ' + name + ' back at position ' + pos(tag) + '.');
      grabbed = null; restore = null;
    }
  });
  order();
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Tags & Chip from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Draggable Tag List Reordering UI
Source: https://codefronts.com/snippets/css-tags-chips/drag-to-reorder/

Reorderable tags with no library — the HTML5 drag API for pointers, plus the part every tutorial skips: full keyboard reordering. Tab to a tag, press Space to lift it (the tag visibly floats), arrow keys walk it through the list, Space drops, Escape cancels — with every move announced through aria-live. Pointer drags compute the insertion point from cursor position, the lifted tag tilts 4° like a picked-up card, and neighbors shuffle aside with real transitions via a 10-line FLIP.
## HTML
```html
<section class="ctc-17" aria-label="Draggable tag reordering demo">
  <div class="ctc-17__stage">
    <div class="ctc-17__wrap">
      <header class="ctc-17__head">
        <p class="ctc-17__kicker">Drag to reorder · pointer + keyboard</p>
        <h1>Grab one. Mouse or Space bar.</h1>
        <p class="ctc-17__sub">Drag with the pointer, or Tab to a tag and press <kbd>Space</kbd> to lift, <kbd>←</kbd><kbd>→</kbd> to move, <kbd>Space</kbd> to drop, <kbd>Esc</kbd> to cancel.</p>
      </header>
      <div class="ctc-17__card">
        <p class="ctc-17__lab" id="ctc17-lab">Interview pipeline stages — priority order</p>
        <div class="ctc-17__list" role="list" aria-labelledby="ctc17-lab">
          <span class="ctc-17__tag" role="listitem" draggable="true" tabindex="0" style="--h:255"><i aria-hidden="true">⠿</i>screening</span>
          <span class="ctc-17__tag" role="listitem" draggable="true" tabindex="0" style="--h:200"><i aria-hidden="true">⠿</i>portfolio</span>
          <span class="ctc-17__tag" role="listitem" draggable="true" tabindex="0" style="--h:150"><i aria-hidden="true">⠿</i>pairing</span>
          <span class="ctc-17__tag" role="listitem" draggable="true" tabindex="0" style="--h:85"><i aria-hidden="true">⠿</i>system design</span>
          <span class="ctc-17__tag" role="listitem" draggable="true" tabindex="0" style="--h:40"><i aria-hidden="true">⠿</i>values</span>
          <span class="ctc-17__tag" role="listitem" draggable="true" tabindex="0" style="--h:320"><i aria-hidden="true">⠿</i>offer</span>
        </div>
        <p class="ctc-17__order">Current order: <output id="ctc17-out"></output></p>
        <p class="ctc-17__sr" aria-live="polite" id="ctc17-live"></p>
      </div>
      <ul class="ctc-17__why" aria-label="What makes this shippable">
        <li><b>Insertion by midpoint</b>dragover finds the first chip whose center is past the cursor — no index math.</li>
        <li><b>10-line FLIP</b>neighbors slide, not teleport: rect before, move, transform back to zero.</li>
        <li><b>Keyboard = first class</b>grab, walk, drop, cancel — every move narrated via <code>aria-live</code>.</li>
      </ul>
    </div>
  </div>
</section>
```
## CSS
```css
.ctc-17,
.ctc-17 *,
.ctc-17 *::before,
.ctc-17 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.ctc-17 {
  --bg: oklch(0.96 0.01 260);
  --card: oklch(0.995 0.002 260);
  --ink: oklch(0.25 0.025 265);
  --mut: oklch(0.5 0.02 265);
  --line: oklch(0.88 0.012 260);
  --acc: oklch(0.55 0.18 262);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
  container-type: inline-size;
}

.ctc-17__stage {
  width: 100%;
  height: 100vh;
  height: 100svh;
  overflow-y: auto;
  display: grid;
  place-items: center;
  padding: clamp(16px,4cqi,48px);
  background: radial-gradient(70cqi 55cqi at 10% -10%,color-mix(in oklch,var(--acc) 13%,transparent),transparent 62%),var(--bg);
  container: ctc17/inline-size;
}

.ctc-17__wrap {
  width: min(640px,100%);
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.ctc-17__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.ctc-17__kicker {
  font-size: 11.5px;
  font-weight: 800;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--acc);
}

.ctc-17__head h1 {
  font-size: clamp(24px,3.8cqi,38px);
  font-weight: 800;
  letter-spacing: -.03em;
  margin: 8px 0 8px;
}

.ctc-17__sub {
  font-size: 13px;
  line-height: 1.6;
  color: var(--mut);
}

.ctc-17__sub kbd {
  font-family: ui-monospace,Menlo,monospace;
  font-size: .85em;
  font-weight: 700;
  background: oklch(1 0 0);
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: 5px;
  padding: 1px 6px;
}

.ctc-17__card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 18px;
  padding: 18px;
  box-shadow: 0 20px 50px -30px oklch(0.3 0.05 265/.5);
}

.ctc-17__lab {
  font-size: 11.5px;
  font-weight: 800;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--mut);
  margin-bottom: 13px;
}

.ctc-17__list {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  min-height: 48px;
}

.ctc-17__tag {
  --c: oklch(0.6 0.14 var(--h,255));
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 0 17px 0 11px;
  border-radius: 12px;
  border: 1.5px solid color-mix(in oklch,var(--c) 30%,transparent);
  background: color-mix(in oklch,var(--c) 11%,oklch(1 0 0));
  color: color-mix(in oklch,var(--c) 72%,var(--ink));
  font-size: 13.5px;
  font-weight: 700;
  cursor: grab;
  user-select: none;
  transition: scale .2s,rotate .2s,box-shadow .2s,opacity .2s,border-color .2s;
}

.ctc-17__tag i {
  font-style: normal;
  font-size: 15px;
  opacity: .5;
  letter-spacing: -2px;
}

.ctc-17__tag:hover {
  border-color: color-mix(in oklch,var(--c) 55%,transparent);
  box-shadow: 0 6px 14px -8px var(--c);
}

.ctc-17__tag:focus-visible {
  outline: 2px solid var(--c);
  outline-offset: 2px;
}

.ctc-17__tag.is-drag {
  opacity: .35;
  scale: .97;
  cursor: grabbing;
}

.ctc-17__tag.is-grab {
  scale: 1.06;
  rotate: 4deg;
  box-shadow: 0 16px 30px -12px var(--c);
  border-color: var(--c);
  background: color-mix(in oklch,var(--c) 20%,oklch(1 0 0));
  z-index: 2;
}

.ctc-17__order {
  margin-top: 14px;
  padding-top: 13px;
  border-top: 1px dashed var(--line);
  font-size: 12.5px;
  color: var(--mut);
}

.ctc-17__order output {
  font-family: ui-monospace,Menlo,monospace;
  font-size: 12px;
  font-weight: 600;
  color: var(--ink);
}

.ctc-17__why {
  display: grid;
  grid-template-columns: repeat(auto-fit,minmax(170px,1fr));
  gap: 10px;
  list-style: none;
}

.ctc-17__why li {
  background: oklch(1 0 0/.65);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 13px 15px;
  font-size: 12px;
  line-height: 1.55;
  color: var(--mut);
}

.ctc-17__why b {
  display: block;
  font-size: 12.5px;
  color: var(--ink);
  margin-bottom: 3px;
}

.ctc-17__why code {
  font-family: ui-monospace,Menlo,monospace;
  font-size: .9em;
  background: oklch(0.93 0.008 260);
  padding: 1px 4px;
  border-radius: 4px;
}

@container ctc17 (width < 480px) {
  .ctc-17__why {
    grid-template-columns: 1fr;
  }
}

@media (prefers-reduced-motion: reduce) {
  .ctc-17 * {
    transition-duration: .01ms !important;
  }

  .ctc-17__tag.is-grab {
    rotate: 0deg;
  }
}
```

## JavaScript
```js
(() => {
  const root = document.querySelector('.ctc-17');
  if (!root || root.dataset.init) return;
  root.dataset.init = '1';
  const list = root.querySelector('.ctc-17__list');
  const live = root.querySelector('#ctc17-live');
  const out = root.querySelector('#ctc17-out');
  let dragging = null, grabbed = null, restore = null;
  const tags = () => [...list.querySelectorAll('.ctc-17__tag')];
  const names = () => tags().map((t) => t.textContent.replace('⠿', '').trim());
  const order = () => { out.textContent = names().join(' → '); };
  const pos = (el) => tags().indexOf(el) + 1;
  const say = (msg) => { live.textContent = msg; };
  const flip = (move) => {
    const before = new Map(tags().map((t) => [t, t.getBoundingClientRect()]));
    move();
    tags().forEach((t) => {
      const b = before.get(t); if (!b) return;
      const a = t.getBoundingClientRect();
      const dx = b.left - a.left, dy = b.top - a.top;
      if (!dx && !dy) return;
      t.animate([{ translate: dx + 'px ' + dy + 'px' }, { translate: '0 0' }], { duration: 220, easing: 'cubic-bezier(.2,.8,.2,1)' });
    });
    order();
  };
  list.addEventListener('dragstart', (e) => {
    dragging = e.target.closest('.ctc-17__tag');
    requestAnimationFrame(() => dragging && dragging.classList.add('is-drag'));
  });
  list.addEventListener('dragover', (e) => {
    e.preventDefault();
    if (!dragging) return;
    const after = tags().filter((t) => t !== dragging).find((t) => {
      const r = t.getBoundingClientRect();
      return e.clientY < r.top || (e.clientY <= r.bottom && e.clientX < r.left + r.width / 2);
    });
    if (after !== dragging.nextSibling) flip(() => (after ? list.insertBefore(dragging, after) : list.append(dragging)));
  });
  list.addEventListener('dragend', () => {
    if (!dragging) return;
    dragging.classList.remove('is-drag');
    say(dragging.textContent.replace('⠿', '').trim() + ' dropped at position ' + pos(dragging) + ' of ' + tags().length + '.');
    dragging = null; order();
  });
  list.addEventListener('keydown', (e) => {
    const tag = e.target.closest('.ctc-17__tag');
    if (!tag) return;
    const name = tag.textContent.replace('⠿', '').trim();
    if (e.key === ' ' || e.key === 'Enter') {
      e.preventDefault();
      if (grabbed === tag) {
        grabbed = null; restore = null; tag.classList.remove('is-grab'); tag.setAttribute('aria-pressed', 'false');
        say(name + ' dropped at position ' + pos(tag) + ' of ' + tags().length + '.');
      } else {
        if (grabbed) { grabbed.classList.remove('is-grab'); }
        grabbed = tag; restore = tags();
        tag.classList.add('is-grab'); tag.setAttribute('aria-pressed', 'true');
        say(name + ' grabbed. Position ' + pos(tag) + ' of ' + tags().length + '. Use arrow keys to move.');
      }
    } else if (grabbed === tag && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) {
      e.preventDefault();
      const sib = e.key === 'ArrowLeft' ? tag.previousElementSibling : tag.nextElementSibling;
      if (sib) {
        flip(() => (e.key === 'ArrowLeft' ? list.insertBefore(tag, sib) : list.insertBefore(sib, tag)));
        tag.focus();
        say(name + ' moved to position ' + pos(tag) + ' of ' + tags().length + '.');
      }
    } else if (grabbed === tag && e.key === 'Escape') {
      flip(() => restore.forEach((t) => list.append(t)));
      grabbed.classList.remove('is-grab'); grabbed.setAttribute('aria-pressed', 'false');
      say('Reorder cancelled. ' + name + ' back at position ' + pos(tag) + '.');
      grabbed = null; restore = null;
    }
  });
  order();
})();
```

How this works

Pointer reordering is three events. dragstart marks the source; dragover (preventDefault-ed — that is what legalizes dropping) finds where the cursor sits and moves the node live, so the list previews its final order mid-drag:

list.addEventListener('dragover', (e) => {
  e.preventDefault();
  const after = [...list.querySelectorAll('[draggable]:not(.is-drag)')]
    .find((el) => e.clientX < el.getBoundingClientRect().left +
                                el.offsetWidth / 2);
  after ? list.insertBefore(dragging, after) : list.append(dragging);
});

Neighbors animate because each insertion runs a micro-FLIP: record every chip's rect, move the node, then transform each chip from old-minus-new position back to zero. Ten lines, and the shuffle reads as physical instead of teleporting.

Keyboard mode is a tiny state machine on each chip (tabindex="0", role="listitem"): Space toggles 'grabbed' (visual float + aria-pressed), ArrowLeft/Right swap the chip with its sibling — reusing the same FLIP — Escape restores the original order captured at grab time. Every action writes a full sentence to the aria-live region: 'design, grabbed. Position 2 of 6.' That sentence is the accessibility contract; without it a blind user has silence.

Make it yours

  • Axis: this list is horizontal (clientX + width). For a vertical list swap to clientY + height — one line.
  • Lift styling: the .is-drag class (scale 1.06, 4° tilt, big shadow) and the CSS-only ghost dimming via the :active-drag pseudo isn't real — the class approach is; restyle it freely.
  • Persistence: read the final order in the 'drop'/keyboard handlers — the demo logs it into the order readout; POST that array where your backend expects it.
  • Drop the tilt for dense admin tables; keep scale ≥1.03 so the lifted item always visually wins its overlaps.

Gotchas — read before shipping

  • Without e.preventDefault() in dragover, the drop event never fires and the cursor shows the no-entry sign — the number-one drag API question on Stack Overflow.
  • The native drag ghost is a screenshot of the element; style the SOURCE (.is-drag) not the ghost — or call setDragImage with an offset if you must customize it.
  • HTML5 drag events do not fire on touch screens — this demo's keyboard mode doubles as the touch fallback (tap to grab, tap arrows), or add a pointer-events port for full touch drag.
  • Announce position changes with aria-live='polite', not 'assertive' — assertive interrupts the screen reader mid-word on every arrow press.

Browser support

ChromeSafariFirefoxEdge
111+16.4+113+111+

The drag API and FLIP via transforms are ancient. Individual scale/rotate properties are 2022+. The floor is oklch()/color-mix() tokens; the interaction itself runs on anything with the drag API (desktop) and the keyboard path everywhere.

Search CodeFronts

Loading…