No.151
CARD CSSJS

Expandable Card

一覧のカードを押すと、その位置から滑らかに広がって詳細表示になる Expandable Card。FLIP の手法で transform だけを動かすので、レイアウトを崩さずに拡大できます。

DEMO

CODE

HTML
<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 &amp; tea" data-bg="linear-gradient(135deg,#E91B89,#7B37A8)"><span class="exc-t">Kyoto</span><span class="exc-s">Temples &amp; tea</span></button></li>
    <li><button type="button" class="exc-card" data-title="Hokkaido" data-sub="Snow &amp; lakes" data-bg="linear-gradient(135deg,#5BAADE,#7B37A8)"><span class="exc-t">Hokkaido</span><span class="exc-s">Snow &amp; lakes</span></button></li>
    <li><button type="button" class="exc-card" data-title="Okinawa" data-sub="Sea &amp; sun" data-bg="linear-gradient(135deg,#0F766E,#5BAADE)"><span class="exc-t">Okinawa</span><span class="exc-s">Sea &amp; 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>
CSS
.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; }
}
JS
(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

Claude Code Cursor v0
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

📖 なかしまぁ先生のCSSアニメーション入門 📖 UIデザインの心理学 ―わかりやすさ・使いやすさの法則 📖 Fundamentals of Web Animation with GSAP

※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。

学びを止めない · AMAZONサブスク

📚 Kindle Unlimited — 技術書・デザイン書が読み放題。30日無料体験 → 🎧 Audible — 移動中に耳で学ぶオーディオブック。30日無料体験 → 📦 Amazon Prime — Prime Reading・配送特典つき。30日無料体験 → 🎵 Music Unlimited — 作業用BGMに。1億曲が聴き放題・30日無料体験 →

※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。

RELATED EFFECTS

↗ MORE
→ FRAMER LAYOUT ANIMATION → FLIP CARD → MODAL SCALE BLUR

Expandable Card(エクスパンダブルカード)とは

一覧に並んだカードを押すと、その場所から広がって詳細表示に変わるカードです。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プロンプトは、クレジット表記なしで自由に使えます。コピーしてそのままプロジェクトに利用できます。