GitHub

Components / Motion / Reorder

Reorder beta

A ranked list that re-sorts itself — rows glide to their new positions instead of jumping.

Preview
Ranked by impact
  • 1Pricing reply98
  • 2Renewal follow-up87
  • 3Thursday demo brief76
  • 4Invoice question64

Uses: Aurora, Glass Panel — include its CSS alongside this one.

<div class="k-aurora" style="max-width:430px">
  <i class="b1"></i><i class="b2"></i><i class="b3"></i>
  <div style="position:relative;padding:20px">
    <div class="k-glass" data-theme="dark">
      <div class="k-glass-h">Ranked by impact <button class="k-rank-btn" data-rank-btn type="button">Re-rank</button></div>
      <ul class="k-rank" data-rank>
      <li data-score="98"><span class="no">1</span>Pricing reply<span class="val">98</span></li>
      <li data-score="87"><span class="no">2</span>Renewal follow-up<span class="val">87</span></li>
      <li data-score="76"><span class="no">3</span>Thursday demo brief<span class="val">76</span></li>
      <li data-score="64"><span class="no">4</span>Invoice question<span class="val">64</span></li>
      </ul>
    </div>
  </div>
</div>
.k-rank{list-style:none;margin:10px 0 0;padding:0;display:grid;gap:7px;font-family:var(--k-font)}
.k-rank li{display:flex;align-items:center;gap:11px;background:color-mix(in srgb,var(--k-on-color) 9%,transparent);border:1px solid color-mix(in srgb,var(--k-on-color) 18%,transparent);border-radius:10px;padding:10px 12px;font-size:13px;color:var(--k-ink)}
.k-rank .no{font-family:var(--k-mono);font-size:11px;color:var(--k-ink-3);width:14px}
.k-rank .val{margin-left:auto;font-size:11.5px;color:var(--k-ink-3)}
.k-rank-btn{font:inherit;font-size:11.5px;font-weight:550;border:1px solid color-mix(in srgb,var(--k-on-color) 25%,transparent);background:transparent;color:var(--k-ink);border-radius:99px;padding:4px 11px;cursor:pointer}
const rankList = document.querySelector('[data-rank]');
const rankBtn = document.querySelector('[data-rank-btn]');
const rankReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
function rankSort() {
  const items = [...rankList.children];
  const first = new Map(items.map(li => [li, li.getBoundingClientRect().top]));
  items.forEach(li => { li.dataset.score = Math.max(40, Math.min(99, +li.dataset.score + Math.round((Math.random() - 0.5) * 36))); });
  items.sort((a, b) => +b.dataset.score - +a.dataset.score).forEach(li => rankList.appendChild(li));
  [...rankList.children].forEach((li, i) => {
    li.querySelector('.no').textContent = i + 1;
    li.querySelector('.val').textContent = li.dataset.score;
    const dy = first.get(li) - li.getBoundingClientRect().top;
    if (dy && !rankReduced) li.animate([{ transform: `translateY(${dy}px)` }, { transform: 'none' }], { duration: 560, easing: 'cubic-bezier(.3,.7,.3,1)' });
  });
}
rankBtn.addEventListener('click', rankSort);
let rankTimer = null;
if (!rankReduced) {
  rankTimer = setInterval(rankSort, 3000);
  rankList.closest('.k-glass').addEventListener('pointerenter', () => clearInterval(rankTimer));
  rankList.closest('.k-glass').addEventListener('pointerleave', () => { clearInterval(rankTimer); rankTimer = setInterval(rankSort, 3000); });
}

Usage

For leaderboards, priority queues and anything whose order is the message. The FLIP technique keeps the motion honest: rows travel, they never teleport.

Accessibility

Rows keep their text and rank numbers after every move, so screen readers follow the final order. Autoplay pauses on hover and never starts under prefers-reduced-motion.

Related parts