Accordion expandWeb · App
- Alpha82
- Bravo47
- Charlie95
- Delta63
Target
Implement the "List reorder (FLIP)" effect in vanilla HTML/CSS/JS. Context: When a list is re-sorted, each row glides from its old position to its new one instead of snapping. Trigger: the element is pressed (pointerdown → pointerup). Timeline: time-based (duration + easing). Motion tokens (normal): - translateY (each row): old position − new position → 0 - duration: 400ms - easing: cubic-bezier(0.2, 0.8, 0.2, 1) - note: First, Last, Invert, Play: measure before and after the DOM change, animate the inverse delta to zero Motion tokens (prefers-reduced-motion): - translateY: — → rows jump; optional 150ms opacity fade - duration: 0ms Technique: plain JavaScript (requestAnimationFrame / class toggles), no animation library required; Web Animations API is acceptable. Acceptance: - animate only transform and opacity unless the spec lists other properties - no layout shift; content is visible without the animation - when prefers-reduced-motion is set, use the reduced tokens only - rows are moved in the DOM (or state) first; only transforms animate - a reorder during an in-flight animation cancels and restarts cleanly
Generated from the spec. Prompts differ by target name only; the tokens are the source of truth.
demo.html
<div class="fx-list-reorder-flip" data-fx-root>
<div class="fx-wrap">
<ul class="fx-list">
<li class="fx-row" data-v="82"><span class="fx-dot" style="background:#4f46e5"></span><span class="fx-name">Alpha</span><span class="fx-val">82</span></li>
<li class="fx-row" data-v="47"><span class="fx-dot" style="background:#818cf8"></span><span class="fx-name">Bravo</span><span class="fx-val">47</span></li>
<li class="fx-row" data-v="95"><span class="fx-dot" style="background:#a5b4fc"></span><span class="fx-name">Charlie</span><span class="fx-val">95</span></li>
<li class="fx-row" data-v="63"><span class="fx-dot" style="background:#c7d2fe"></span><span class="fx-name">Delta</span><span class="fx-val">63</span></li>
</ul>
<button type="button" class="fx-sort">Sort by value</button>
</div>
</div>
demo.css
.fx-list-reorder-flip { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; }
.fx-list-reorder-flip .fx-wrap { display: flex; flex-direction: column; gap: 10px; width: 64%; max-width: 320px; }
.fx-list-reorder-flip .fx-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.fx-list-reorder-flip .fx-row { display: flex; align-items: center; gap: 10px; height: 34px; padding: 0 12px; border-radius: 8px; background: #fff; box-shadow: 0 0 0 1px var(--fx-muted, #e4e4e7); font: 500 13px/1 Inter, system-ui, sans-serif; color: var(--fx-ink, #18181b); will-change: transform; }
.fx-list-reorder-flip .fx-dot { width: 10px; height: 10px; border-radius: 50%; }
.fx-list-reorder-flip .fx-name { flex: 1; }
.fx-list-reorder-flip .fx-val { font-variant-numeric: tabular-nums; color: #71717a; }
.fx-list-reorder-flip .fx-sort { align-self: flex-end; height: 30px; padding: 0 12px; border: 1px solid var(--fx-muted, #e4e4e7); border-radius: 8px; background: #fff; font: 500 12px/1 Inter, system-ui, sans-serif; cursor: pointer; }
demo.js
export default function mount(root) {
const list = root.querySelector('.fx-list'), btn = root.querySelector('.fx-sort');
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
let desc = false;
function reorder(rows) {
const first = new Map(rows.map((r) => [r, r.getBoundingClientRect().top])); // F
rows.forEach((r) => list.appendChild(r)); // DOM change
rows.forEach((r) => {
const dy = first.get(r) - r.getBoundingClientRect().top; // L + I
r.getAnimations().forEach((a) => a.cancel());
if (reduced || dy === 0) { if (reduced) r.animate([{ opacity: .4 }, { opacity: 1 }], { duration: 150 }); return; }
r.animate([{ transform: `translateY(${dy}px)` }, { transform: 'translateY(0)' }], { duration: 400, easing: 'cubic-bezier(.2, .8, .2, 1)' }); // P
});
}
const sort = () => { desc = !desc; const rows = Array.from(list.children).sort((a, b) => desc ? b.dataset.v - a.dataset.v : a.dataset.v - b.dataset.v); reorder(rows); };
const shuffle = () => { const rows = Array.from(list.children).sort(() => Math.random() - 0.5); reorder(rows); };
btn.addEventListener('click', sort);
let t1, t2;
return {
play() { clearTimeout(t1); clearTimeout(t2); t1 = setTimeout(shuffle, 300); t2 = setTimeout(() => { desc = false; sort(); }, 1500); },
stop() { clearTimeout(t1); clearTimeout(t2); },
};
}
Inspired by aerotwist.com — Paul Lewis's FLIP technique. The preview is an original re-creation; the effect name belongs to its author.
Measure → mutate → measure → animate the inverse delta to zero. Cancel running animations on each row before starting a new one, otherwise a fast double-click stacks transforms.
Added 2026-11-09 · Report an issue