DEMO
CODE
<div class="demo-stage flp-stage">
<div class="flp-box">
<div class="flp-tools">
<button type="button" class="flp-btn" id="flp-shuffle">シャッフル</button>
<button type="button" class="flp-btn" id="flp-sort">番号順に戻す</button>
</div>
<ul class="flp-grid" id="flp-grid" aria-label="並べ替えるカード">
<li class="flp-item" data-n="1">01</li>
<li class="flp-item" data-n="2">02</li>
<li class="flp-item" data-n="3">03</li>
<li class="flp-item" data-n="4">04</li>
<li class="flp-item" data-n="5">05</li>
<li class="flp-item" data-n="6">06</li>
</ul>
<p class="flp-status" id="flp-status" role="status" aria-live="polite"></p>
</div>
</div>
.flp-stage { display:flex; align-items:center; justify-content:center; padding:1.5rem; background:#0B0B10; min-height:320px; }
.flp-box { width:100%; max-width:340px; display:flex; flex-direction:column; gap:.9rem; }
.flp-tools { display:flex; gap:.6rem; flex-wrap:wrap; }
.flp-btn { min-height:44px; padding:0 1rem; border-radius:10px; border:1px solid rgba(255,255,255,.2); background:transparent; color:#EDEDED; font-size:.82rem; cursor:pointer; }
.flp-btn:focus-visible { outline:2px solid #E91B89; outline-offset:2px; }
.flp-grid { list-style:none; margin:0; padding:0; display:grid; grid-template-columns:repeat(3, 1fr); gap:.6rem; }
.flp-item { display:flex; align-items:center; justify-content:center; aspect-ratio:1; border-radius:12px; color:#fff; font-family:'Oswald',sans-serif; font-size:1.3rem; letter-spacing:.06em; }
.flp-item[data-n="1"] { background:#7B37A8; }
.flp-item[data-n="2"] { background:#D1177A; }
.flp-item[data-n="3"] { background:#0369A1; }
.flp-item[data-n="4"] { background:#047857; }
.flp-item[data-n="5"] { background:#B45309; }
.flp-item[data-n="6"] { background:#4338CA; }
.flp-status { margin:0; min-height:1.2em; color:#A1A1AA; font-size:.76rem; }
(function(){
var grid = document.getElementById('flp-grid');
var shuffleBtn = document.getElementById('flp-shuffle');
var sortBtn = document.getElementById('flp-sort');
var status = document.getElementById('flp-status');
if (!grid || !shuffleBtn || !sortBtn || !status) return;
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
function items(){ return Array.prototype.slice.call(grid.children); }
function reorder(next, msg){
var first = new Map();
items().forEach(function(el){
first.set(el, el.getBoundingClientRect());
if (el.getAnimations) el.getAnimations().forEach(function(a){ a.cancel(); });
});
next.forEach(function(el){ grid.appendChild(el); });
status.textContent = msg + ':' + next.map(function(el){ return el.textContent; }).join('、');
if (reduce.matches || !grid.animate) return;
next.forEach(function(el){
var f = first.get(el), l = el.getBoundingClientRect();
var dx = f.left - l.left, dy = f.top - l.top;
if (!dx && !dy) return;
el.animate([{ transform: 'translate(' + dx + 'px,' + dy + 'px)' }, { transform: 'none' }],
{ duration: 450, easing: 'cubic-bezier(.22,1,.36,1)' });
});
}
shuffleBtn.addEventListener('click', function(){
var list = items(), next;
do {
next = list.slice();
for (var i = next.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1)), t = next[i];
next[i] = next[j]; next[j] = t;
}
} while (next.every(function(el, k){ return el === list[k]; }));
reorder(next, 'シャッフルしました');
});
sortBtn.addEventListener('click', function(){
reorder(items().sort(function(a, b){ return a.dataset.n - b.dataset.n; }), '番号順に戻しました');
});
})();
AI PROMPT
シャッフルや並べ替えでカードの順番が変わった時、各カードが元の位置から新しい位置へなめらかにすべる演出を、FLIP(First・Last・Invert・Play)の手順で作って。6枚のカードを ul と li で3列の CSS Grid に並べる。並べ替える時は、①First:各カードの getBoundingClientRect を記録する(動いている途中なら、その時点で見えている位置が記録される)。続けて getAnimations() で動いている途中のアニメーションを cancel する。②Last:appendChild で新しい順番に並べ替える。③Invert:並べ替えた後の getBoundingClientRect との差(元の位置 − 新しい位置)を求める。④Play:element.animate で transform: translate(差X, 差Y) から transform: none へ450ミリ秒・cubic-bezier(.22,1,.36,1) で動かす(位置が変わらないカードは動かさない)。レイアウトは1回だけ変え、動きは transform だけで作る(top や left は動かさない)。シャッフルは Fisher–Yates で行い、結果が元と同じ順番なら引き直す。「シャッフル」と「番号順に戻す」のボタンを置き、連打しても途中の位置から次の位置へ動くようにする。並べ替えた結果は role="status" の段落で「シャッフルしました:03、01、…」と伝える。prefers-reduced-motion: reduce の時は動かさずに並べ替えだけ行う。 Acceptance criteria: works with prefers-reduced-motion (animation disabled), zero console errors, zero layout shift
AIエージェントでの実装ガイド → Claude Code でWebアニメーションを実装する
BOOKS · PR · AMAZON
※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。
学びを止めない · AMAZONサブスク
※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。
CREATOR TOOLS · スクール(PR)
※本サイトはアフィリエイトリンクを含みます。リンク経由のご購入・お申し込みで運営者が紹介料を受け取る場合があります。
並べ替えやシャッフルでカードの順番が変わった時、それぞれのカードが元の場所から新しい場所へすべって移動する演出です。Paul Lewis さんの記事「FLIP Your Animations」で紹介された、First・Last・Invert・Play の頭文字を取った FLIP という手順で作っています。
当サイトの Framer Layout Animation は要素の幅が変わると兄弟要素もなめらかに再配置される Framer Motion の演出、Animated List は新着が上から差し込まれ既存の行が押し出される通知リスト、View Transition API はブラウザ標準の API で画面の切り替えを滑らかにする演出です。本ページはライブラリを使わず、並べ替えそのものを素の JavaScript で動かす別テーマです。
一覧の並べ替え・絞り込み・お気に入りへの移動など、「どこへ行ったか」が分かると迷わない操作に向いています。動きは transform だけで作れるので軽く、動きを減らす設定の利用者には並べ替えだけを行えば、機能はそのまま使えます。
FLIP とは何の略ですか?
Paul Lewis さんが記事「FLIP Your Animations」で紹介した手法で、First(最初の位置)・Last(最後の位置)・Invert(差を打ち消す)・Play(再生)の頭文字です。先に DOM を最終的な形へ変えてしまい、見た目だけを transform で元の位置へ戻してから、transform を外すアニメーションを再生します。位置の計算はブラウザのレイアウトに任せられるので、グリッドや折り返しのある並びでも同じ書き方で動かせます。
連打した時にカードが飛ばないのはなぜですか?
並べ替える前の位置を getBoundingClientRect で測ると、動いている途中のカードは transform を含めた「今見えている位置」が返ります。その位置を First として記録してから、動いている途中のアニメーションを cancel しています。こうすると、途中の位置から次の位置へ続けて動くため、連打しても元の場所へ一瞬戻るようなちらつきが起きません。
コードは無料で使えますか?
はい。Motion LabのコードスニペットとAIプロンプトは、クレジット表記なしで自由に使えます。コピーしてそのままプロジェクトに利用できます。