本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100
セクション

料金表のセクションで列が順に立ち上がり、おすすめの列が一段高くなる

デモ

操作:カーソルを重ねる。 「さわってみる」を押すと自動再生が止まり、マウスで動かせます。

仕様

動作
DOM
種類
レシピ
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

料金表の 3 列のカードを、画面に入ったときに左から順に起こして見せます。カードは下の辺を軸に奥へ 90° 寝かせておき、0.15 秒ずつずらして立てていきます。3 枚が立ちそろうと、真ん中のおすすめの列だけ地が上へ 48px 伸び、伸びた所に「おすすめ」の札が左から開いて付きます。並んだ表のなかで、おすすめの列が最後にもう一段せり上がるので、視線がそこへ集まります。

起き上がりは 0.9 秒の back.out で、立ちきる直前に手前へ少し倒れ込んでから戻ります。遠近は列ごとに付け、消失点を列の下の辺に置いています。寝ているカードは真横から見た線になって見えず、起き上がるにつれて高さが出てきます。傾いている間は墨の陰を重ね、濃さを 45% から 0 へ下げて、白いカードでも向きが変わっているのが分かるようにしています。

おすすめの列の地は、カードそのものではなく ::before に塗り、上へ伸びる量を CSS の変数で持たせています。カードの高さを変えると、中の文字やボタンの位置までずれ、ほかの列と下の辺がそろわなくなるためです。札も伸びた地の中に置くので、伸ばす高さと札の位置は同じ変数から決めます。

出すのは画面に入った 1 回だけなので、スクロールの位置を追い続ける仕組みは要らず、IntersectionObserver で、表の上端が画面の下から 20% の線を越えたときに再生します。寝ているカードも読み上げと Tab の移動には残るので、表の中に焦点が入ったら、線を越える前でも出しきります。列に重ねるか、中のボタンに焦点が入ると、その列が 8px 浮きます。動きを減らす設定では、出そろった表をそのまま見せます。

つまみは、起き上がる時間とずらす間隔、行き過ぎの強さ、地の伸びる高さです。行き過ぎを強くしすぎると、カードが手前へ大きく倒れ込み、隣の列に迫って見えます。月額と年額の切り替えで数字を回すような料金表と違い、表そのものの現れ方でおすすめを印象づける形です。サービスの料金表や、プランの比べ、会員の種類の紹介に向いています。

コード

HTML
<section class="pricing" aria-labelledby="pricing-title">
  <p class="pricing__eyebrow">Pricing</p>
  <h2 class="pricing__title" id="pricing-title">サイトの保守プラン</h2>
  <ul class="pricing__plans">
    <li class="pricing__plan">
      <article class="pricing__card">
        <h3 class="pricing__name">ライト</h3>
        <p class="pricing__text">ここに説明文が入ります。</p>
        <p class="pricing__price"><strong>¥9,800</strong>/月</p>
        <ul class="pricing__features">
          <li>サーバーの監視</li>
          <li>月1回のバックアップ</li>
          <li>メールでの相談</li>
        </ul>
        <a class="pricing__cta" href="/contact/?plan=lite">申し込む</a>
      </article>
    </li>
    <!-- おすすめの列。地が上へ一段伸び、伸びた所に札が付く -->
    <li class="pricing__plan is-featured">
      <article class="pricing__card">
        <p class="pricing__badge">おすすめ</p>
        <h3 class="pricing__name">スタンダード</h3>
        <p class="pricing__text">ここに説明文が入ります。</p>
        <p class="pricing__price"><strong>¥29,800</strong>/月</p>
        <ul class="pricing__features">
          <li>月4回までの更新</li>
          <li>アクセスの月次レポート</li>
          <li>チャットでの相談</li>
        </ul>
        <a class="pricing__cta" href="/contact/?plan=standard">申し込む</a>
      </article>
    </li>
    <li class="pricing__plan">
      <article class="pricing__card">
        <h3 class="pricing__name">プレミアム</h3>
        <p class="pricing__text">ここに説明文が入ります。</p>
        <p class="pricing__price"><strong>¥59,800</strong>/月</p>
        <ul class="pricing__features">
          <li>更新の回数は無制限</li>
          <li>新しいページを月1本</li>
          <li>専任の担当者</li>
        </ul>
        <a class="pricing__cta" href="/contact/?plan=premium">相談する</a>
      </article>
    </li>
  </ul>
</section>

CSS
.pricing {
  --bg: #f2f1ed;
  --surface: #ffffff;
  --ink: #262626;
  --mute: #6b6b6b;
  --line: rgba(38, 38, 38, 0.12);
  --step: 48px; /* おすすめの列が上へ伸びる高さ */
  padding: 96px 24px;
  background: var(--bg);
  color: var(--ink);
  text-align: center;
}

.pricing__eyebrow { margin: 0 0 12px; font-size: 13px; font-weight: 700; line-height: 1; letter-spacing: 0.14em; text-transform: uppercase; color: var(--mute); }

.pricing__title { margin: 0 0 32px; font-size: clamp(28px, 4vw, 44px); font-weight: 800; line-height: 1.1; letter-spacing: -0.02em; text-wrap: balance; }

.pricing__plans {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 320px));
  justify-content: center;
  gap: 24px;
  margin: 0;
  padding: var(--step) 0 0;
  list-style: none;
  text-align: left;
}

/* 列ごとに遠近を持ち、消失点を下の辺に置く(寝ているカードは線になる) */
.pricing__plan { perspective: 1200px; perspective-origin: 50% 100%; transition: transform 0.3s ease; }
.pricing__plan:hover,
.pricing__plan:focus-within { transform: translateY(-8px); }

/* 地は ::before に塗り、--rise の分だけ上へ伸ばす。::after は起き上がる間の陰 */
.pricing__card {
  --rise: 0px;
  position: relative;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  height: 100%;
  padding: 32px;
}

.pricing__card::before,
.pricing__card::after { content: ""; position: absolute; inset: 0; border-radius: 4px; }
.pricing__card::before { top: calc(-1 * var(--rise)); z-index: -1; background: var(--surface); }
.pricing__card::after { background: rgb(38 38 38 / var(--shade, 0)); pointer-events: none; }

/* おすすめは明暗を反転する */
.is-featured .pricing__card {
  --rise: var(--step);
  --surface: #262626;
  --ink: #eeece6;
  --mute: #a8a6a0;
  --line: rgba(238, 236, 230, 0.14);
  color: var(--ink);
}

.pricing__badge {
  position: absolute;
  top: calc(10px - var(--rise)); /* 伸びる地の上端に付いて一緒に上がる */
  left: 32px;
  margin: 0;
  padding: 7px 14px;
  border-radius: 999px;
  background: var(--ink);
  color: #111111;
  font-size: 13px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.06em;
}

.pricing__name { margin: 0; font-size: 22px; font-weight: 700; line-height: 1.3; letter-spacing: -0.01em; }
.pricing__text { margin: 8px 0 0; font-size: 14px; line-height: 1.7; letter-spacing: 0.04em; color: var(--mute); }
.pricing__price { margin: 24px 0 0; font-size: 14px; font-weight: 600; line-height: 1; color: var(--mute); }
.pricing__price strong { margin-right: 4px; font-size: 44px; font-weight: 800; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; color: var(--ink); }

.pricing__features { display: grid; gap: 10px; margin: 24px 0 32px; padding: 16px 0 0; border-top: 1px solid var(--line); list-style: none; font-size: 14px; line-height: 1.7; letter-spacing: 0.04em; }
.pricing__features li { display: flex; align-items: center; gap: 12px; }
.pricing__features li::before { content: ""; flex: none; width: 10px; height: 5px; margin: 0 2px 3px; border: solid currentColor; border-width: 0 0 2px 2px; rotate: -45deg; }

.pricing__cta { display: flex; align-items: center; justify-content: center; height: 48px; margin-top: auto; border-radius: 999px; box-shadow: inset 0 0 0 1px var(--ink); color: var(--ink); font-size: 15px; font-weight: 600; letter-spacing: 0.04em; text-decoration: none; }
.is-featured .pricing__cta { background: var(--ink); color: #111111; }
.pricing :focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }

@media (max-width: 800px) {
  .pricing__plans { grid-template-columns: minmax(0, 400px); padding-top: 0; }
  .is-featured { margin-top: var(--step); }
}

@media (prefers-reduced-motion: reduce) {
  .pricing__plan { transition: none; }
}

JavaScript
import { gsap } from "gsap";

const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

document.querySelectorAll(".pricing").forEach((section) => {
  // 動きを減らす設定では、出そろった表のまま見せる
  if (reduceMotion) return;
  const list = section.querySelector(".pricing__plans");
  const cards = section.querySelectorAll(".pricing__card");
  const featured = section.querySelector(".is-featured .pricing__card");
  const badge = section.querySelector(".pricing__badge");
  const step = getComputedStyle(section).getPropertyValue("--step").trim();

  const tl = gsap
    .timeline({ paused: true })
    // 下の辺を軸に、奥へ寝た所から左の列から順に起き上がる。寝ているほど陰を濃くする
    .fromTo(
      cards,
      { rotationX: 90, "--shade": 0.45 },
      { rotationX: 0, "--shade": 0, transformOrigin: "50% 100%", duration: 0.9, ease: "back.out(1.3)", stagger: 0.15 }
    )
    // 立ちそろったら、おすすめの地を上へ一段伸ばし、伸びる途中で札を左から開く
    .fromTo(featured, { "--rise": "0px" }, { "--rise": step, duration: 0.6, ease: "power3.out" }, ">")
    .fromTo(
      badge,
      { clipPath: "inset(0% 100% 0% 0% round 99px)" },
      { clipPath: "inset(0% 0% 0% 0% round 99px)", duration: 0.3, ease: "power3.out" },
      "<0.15"
    );

  // 表の上端が画面の下から 20% の線を越えたら、1 回だけ出す
  const io = new IntersectionObserver(
    ([entry]) => {
      if (!entry.isIntersecting) return;
      io.disconnect();
      tl.play();
    },
    { rootMargin: "0px 0px -20% 0px" }
  );
  io.observe(list);

  // Tab で表の中のボタンに来たら、線を越える前でも出しきる(寝たカードに焦点が当たったままにしない)
  section.addEventListener("focusin", () => tl.play());
});

インタラクションデザインについて、
お気軽にご相談ください。

ここに並べているのは、Web サイトの中で動く UI のサンプルです。新しく作るサイトに組み込むことも、いま公開中のサイトに、ボタンやヒーロー、スクロールの演出だけを足すこともできます。設計から実装までの進め方はWeb サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。