DEMO
カードを押した位置から広がり、閉じると元の位置へ戻ります。一覧と詳細が同じものだと目で追えるため、今どこを見ているかを見失いません。
CODE
<div class="demo-stage exc-stage" id="exc-stage">
<ul class="exc-list" id="exc-list">
<li><button type="button" class="exc-card" data-title="Kyoto" data-sub="Temples & tea" data-bg="linear-gradient(135deg,#E91B89,#7B37A8)"><span class="exc-t">Kyoto</span><span class="exc-s">Temples & tea</span></button></li>
<li><button type="button" class="exc-card" data-title="Hokkaido" data-sub="Snow & lakes" data-bg="linear-gradient(135deg,#5BAADE,#7B37A8)"><span class="exc-t">Hokkaido</span><span class="exc-s">Snow & lakes</span></button></li>
<li><button type="button" class="exc-card" data-title="Okinawa" data-sub="Sea & sun" data-bg="linear-gradient(135deg,#0F766E,#5BAADE)"><span class="exc-t">Okinawa</span><span class="exc-s">Sea & sun</span></button></li>
</ul>
<div class="exc-detail" id="exc-detail" role="dialog" aria-modal="true" aria-labelledby="exc-dt" hidden>
<div class="exc-hero" id="exc-hero"><span class="exc-t" id="exc-dt"></span><span class="exc-s" id="exc-ds"></span></div>
<p class="exc-body">カードを押した位置から広がり、閉じると元の位置へ戻ります。一覧と詳細が同じものだと目で追えるため、今どこを見ているかを見失いません。</p>
<button type="button" class="exc-close" id="exc-close" aria-label="閉じる">×</button>
</div>
</div>
.exc-stage { position:relative; display:flex; align-items:center; justify-content:center; padding:1.5rem; background:#0B0B10; min-height:320px; overflow:hidden; }
.exc-list { list-style:none; margin:0; padding:0; display:grid; grid-template-columns:repeat(3,1fr); gap:.8rem; width:100%; max-width:440px; }
.exc-card { width:100%; height:130px; border:0; border-radius:14px; padding:.9rem; display:flex; flex-direction:column; justify-content:flex-end; align-items:flex-start; color:#fff; cursor:pointer; text-align:left; transition: transform .2s ease; }
.exc-card:hover { transform:translateY(-3px); }
.exc-card:focus-visible, .exc-close:focus-visible { outline:2px solid #fff; outline-offset:3px; }
.exc-t { font-family:'Oswald',sans-serif; font-size:1.05rem; letter-spacing:.06em; }
.exc-s { font-size:.72rem; opacity:.8; }
.exc-detail { position:absolute; inset:1.2rem; background:#16161C; border-radius:16px; overflow:hidden; transform-origin:top left; box-shadow:0 30px 60px -20px rgba(0,0,0,.8); }
.exc-detail[hidden] { display:none; }
.exc-hero { height:130px; padding:1rem 1.2rem; display:flex; flex-direction:column; justify-content:flex-end; color:#fff; }
.exc-hero .exc-t { font-size:1.6rem; }
.exc-body { margin:0; padding:1rem 1.2rem; color:#C9C9D1; font-size:.85rem; line-height:1.7; opacity:0; transition: opacity .25s ease .15s; }
.exc-detail.is-open .exc-body { opacity:1; }
.exc-close { position:absolute; top:.6rem; right:.6rem; width:36px; height:36px; border:0; border-radius:50%; background:rgba(0,0,0,.35); color:#fff; font-size:1.1rem; cursor:pointer; }
@media (prefers-reduced-motion: reduce) {
.exc-card, .exc-body { transition-duration:.01ms; }
}
(function(){
var stage = document.getElementById('exc-stage');
var detail = document.getElementById('exc-detail');
var hero = document.getElementById('exc-hero');
var closeBtn = document.getElementById('exc-close');
if (!stage || !detail || !hero || !closeBtn) return;
var cards = stage.querySelectorAll('.exc-card');
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
var from = null;
cards.forEach(function(c){ c.style.background = c.dataset.bg; });
function invert(rect){
var d = detail.getBoundingClientRect();
return 'translate(' + (rect.left - d.left) + 'px,' + (rect.top - d.top) + 'px) scale(' + (rect.width / d.width) + ',' + (rect.height / d.height) + ')';
}
function play(el, a, b, done){
if (reduce.matches || !el.animate) { done && done(); return; }
el.animate([{ transform: a }, { transform: b }], { duration: 420, easing: 'cubic-bezier(.22,1,.36,1)' }).onfinish = done || null;
}
function open(card){
from = card;
hero.style.background = card.dataset.bg;
document.getElementById('exc-dt').textContent = card.dataset.title;
document.getElementById('exc-ds').textContent = card.dataset.sub;
detail.hidden = false;
play(detail, invert(card.getBoundingClientRect()), 'none');
requestAnimationFrame(function(){ detail.classList.add('is-open'); });
closeBtn.focus();
}
function close(){
if (detail.hidden) return;
detail.classList.remove('is-open');
var card = from;
play(detail, 'none', invert(card.getBoundingClientRect()), function(){ detail.hidden = true; card.focus(); });
}
cards.forEach(function(c){ c.addEventListener('click', function(){ open(c); }); });
closeBtn.addEventListener('click', close);
detail.addEventListener('keydown', function(e){
if (e.key === 'Escape') close();
else if (e.key === 'Tab') { e.preventDefault(); closeBtn.focus(); }
});
})();
AI PROMPT
一覧のカードを押すと、その位置から滑らかに広がって詳細パネルになる Expandable Card を作って。FLIP(First・Last・Invert・Play)の手法を使う:詳細パネルを最終位置(ステージいっぱい)で表示してから、押したカードとの位置と大きさの差を getBoundingClientRect で測り、translate(差分) scale(幅の比, 高さの比) を初期状態として Web Animations API の element.animate() で none へ戻す(transform-origin: top left)。top/left/width/height を直接動かさないので、レイアウトの再計算が起きずに滑らかに拡大する。閉じる時は逆向きに再生し、終わったら hidden に戻して、押したカードへフォーカスを戻す。本文は拡大が終わる頃に opacity で遅れて出す。詳細パネルは role="dialog" aria-modal="true" aria-labelledby で見出しと関連付け、開いたら閉じるボタンにフォーカスし、開いている間は Tab でパネルの外へ出ないようにし、Esc でも閉じられるようにする。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アソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。
一覧に並んだカードを押すと、その場所から広がって詳細表示に変わるカードです。Aceternity UI の Expandable Card で知られ、商品一覧や記事一覧から詳細へ移る時に「どのカードを開いたか」を目で追えるようにする演出として使われます。
当サイトの Framer Layout Animation は Framer Motion の layout 機能で要素の移動を補間する演出、Flip Card はカードの表裏を回転させる演出です。本ページはライブラリを使わずに FLIP の手法と Web Animations API だけで、一覧から詳細への拡大を作る別テーマです。
見た目の滑らかさは、transform だけを動かすことで決まります。あわせて、開いた時のフォーカス移動、Esc で閉じること、閉じたら元のカードへフォーカスを戻すことを入れると、キーボードでも迷わない詳細表示になります。
FLIP とはどんな手法ですか?
First(最初の位置を測る)・Last(最終の位置で描く)・Invert(差分を transform で打ち消して最初の位置に見せる)・Play(transform を外して最終位置へ動かす)の頭文字です。実際のレイアウトは最初から最終状態にしておき、見た目だけを transform で動かすため、位置や大きさが大きく変わる演出でも滑らかに再生できます。
拡大中に文字が引き伸ばされて見えませんか?
scale で縦横比の違う拡大をすると、途中の文字や角丸が一時的に歪みます。本実装では本文を拡大が終わる頃に遅れて表示し、歪みが目立つ要素を動きの間は見せないようにしています。見出しも歪ませたくない場合は、見出しだけ逆向きの scale をかけて打ち消す方法があります。
コードは無料で使えますか?
はい。Motion LabのコードスニペットとAIプロンプトは、クレジット表記なしで自由に使えます。コピーしてそのままプロジェクトに利用できます。