本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100

セクション

スクロールでセクションのベントグリッドのタイルが順に現れる

デモ

操作:スクロール。 「さわってみる」を押すと自動再生が止まり、マウスで動かせます。 触っている間は、ホイールでデモの中がスクロールします。

仕様

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

しくみと調整

セクションまでスクロールすると、大小のタイルが下の縁から上へ向かって開き、文字と写真がそれを少し遅れて追いかけてきます。文字は下から上がり、写真はわずかに拡大した状態から元の大きさへ戻るので、タイルの中から内容が湧き上がってくるように見えます。写真が戻りきるのはタイルが開いたあとなので、開き終えてからも、少しだけ動きが残ります。

開く動きは clip-path の inset で、0.9秒の power3.out。中身は0.08秒だけ遅らせて、同じ長さで上げています。同じタイミングで画面に入ったタイルは0.12秒ずつずらして開くので、横に並んでいれば左から順に開いていきます。この間隔を0.2秒より広げると、最後のタイルが開くまでの待ち時間が目立ってきます。

開くのは画面に入ったときの一度きりなので、スクロール位置を追い続ける ScrollTrigger は使わず、ブラウザの IntersectionObserver を選びました。rootMargin で画面の下側を少し縮めておき、タイルの上端がその線を越えたら通知を受けます。同時に線を越えたタイルはひとつの通知にまとまって届くので、その中の順番で開始をずらせば、順に開く動きになります。タイルの大きさは CSS グリッドの span で決めているので、配置を組み替えても、開く順は HTML に書いた順のままです。

まだ開いていないタイルは、clip-path だけで隠しています。visibility や display で隠すと中身がスクリーンリーダーに届かなくなりますが、clip-path なら見た目が隠れるだけで、文章はそのまま読めます。隠すための値もスクリプトが入れるので、スクリプトが動かない環境では、タイルは最初から開いた状態で並びます。ページの途中で読み込み直したときは、画面より上のタイルが閉じたまま残り、スクロールで戻って画面に入った時点で開きます。

コード

HTML
<!-- タイルの大きさは tile--feature(2×2)・tile--wide(2×1)・tile--tall(1×2)で決める。表示される順は、並べた順 -->
<section class="bento" aria-labelledby="bento-title">
  <div class="bento__head">
    <p class="bento__eyebrow">What we do</p>
    <h2 class="bento__title" id="bento-title"><span>公開に必要なすべてを、</span><span>ひとつのチームで。</span></h2>
    <p class="bento__lead">ここに説明文が入ります。</p>
  </div>
  <ul class="bento__grid">
    <li class="tile tile--feature tile--photo">
      <img class="tile__image" src="/img/brand-sites.jpg" alt="" width="1072" height="832">
      <div class="tile__body">
        <h3 class="tile__title">ブランドサイト</h3>
        <p class="tile__text">ここに説明文が入ります。</p>
      </div>
    </li>
    <li class="tile tile--wide">
      <div class="tile__body">
        <h3 class="tile__title">デザインシステム</h3>
        <p class="tile__text">ここに説明文が入ります。</p>
      </div>
    </li>
    <li class="tile tile--dark">
      <div class="tile__body">
        <p class="tile__label">満足度</p>
        <p class="tile__stat">4.9<small>/5</small></p>
        <p class="tile__text">制作後のアンケート(5段階)</p>
      </div>
    </li>
    <li class="tile tile--tall tile--photo">
      <img class="tile__image" src="/img/motion.jpg" alt="" width="520" height="832">
      <div class="tile__body">
        <h3 class="tile__title">モーション</h3>
        <p class="tile__text">ここに説明文が入ります。</p>
      </div>
    </li>
    <li class="tile">
      <div class="tile__body">
        <h3 class="tile__title">検索対策</h3>
        <p class="tile__text">ここに説明文が入ります。</p>
      </div>
    </li>
    <li class="tile tile--wide tile--accent">
      <div class="tile__body">
        <h3 class="tile__title">運用と改善</h3>
        <p class="tile__text">ここに説明文が入ります。</p>
      </div>
    </li>
  </ul>
</section>

CSS
.bento {
  --ink: #262626;
  --surface: #ffffff;
  --mute-small: #6b6b6b;
  --dark: #262626;          /* 数字を見せるタイル */
  --dark-ink: #eeece6;
  --accent: #d9ba92;        /* 背景色を塗ったタイル(アクセントカラー。上の文字は暗い色) */
  padding: 96px 24px;
  background: #f2f1ed;
  color: var(--ink);
}

.bento__head { max-width: 640px; margin: 0 auto; text-align: center; }
.bento__eyebrow { margin: 0; font-size: 13px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--mute-small); }
.bento__title { margin: 16px 0 0; font-size: clamp(28px, 5vw, 44px); font-weight: 800; line-height: 1.1; letter-spacing: -0.02em; }
.bento__title span { display: block; }
/* 和文を文節の切れ目で折り返す(対応していないブラウザでは、ふつうの折り返しになる) */
.bento__lead,
.tile__text { word-break: auto-phrase; }
.bento__lead { margin: 24px 0 0; font-size: 18px; line-height: 1.8; letter-spacing: 0.04em; }

.bento__grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: 200px;
  gap: 16px;
  max-width: 1088px;
  margin: 64px auto 0;
  padding: 0;
  list-style: none;
}

.tile {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  overflow: hidden;
  padding: 24px;
  border-radius: 4px;
  background: var(--surface);
}

.tile--feature { grid-column: span 2; grid-row: span 2; }
.tile--wide { grid-column: span 2; }
.tile--tall { grid-row: span 2; }
.tile--dark { background: var(--dark); color: var(--dark-ink); }
.tile--accent { background: var(--accent); }
.tile--photo { color: #ffffff; }

.tile__image { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* 写真の上の文字を読みやすくするために重ねるグラデーション(下ほど濃い) */
.tile--photo::after { content: ""; position: absolute; inset: 0; background: linear-gradient(0deg, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0) 60%); }
.tile__body { position: relative; z-index: 1; }

.tile__title { margin: 0; font-size: 24px; font-weight: 700; line-height: 1.3; letter-spacing: -0.01em; }
.tile__text { margin: 8px 0 0; font-size: 14px; line-height: 1.7; letter-spacing: 0.04em; }
.tile:not(.tile--photo, .tile--dark, .tile--accent) .tile__text { color: var(--mute-small); }
.tile__stat { margin: 0; font-size: 64px; font-weight: 800; line-height: 1; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.tile__stat small { margin-left: 4px; font-size: 20px; font-weight: 700; color: #a8a6a0; }
.tile--dark .tile__text { font-size: 13px; color: #a8a6a0; }
/* 数字のタイルは、ラベルを上端、数字と説明を下端に置く */
.tile--dark .tile__body { flex: 1; display: flex; flex-direction: column; }
.tile__label { margin: 0 0 auto; font-size: 13px; font-weight: 700; line-height: 1; letter-spacing: 0.06em; color: #a8a6a0; }

/* 狭い画面では 2 列、さらに狭ければ 1 列にする(大きいタイルは 2 段ぶんの高さのまま) */
@media (max-width: 800px) {
  .bento__grid { grid-template-columns: repeat(2, 1fr); grid-auto-rows: 180px; }
}

@media (max-width: 480px) {
  .bento__grid { grid-template-columns: 1fr; }
  .tile--feature,
  .tile--wide { grid-column: auto; }
}

JavaScript
import { gsap } from "gsap";

const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const STAGGER = 0.12; // 同じときに線を越えたタイルどうしの間隔(秒)

// タイルの現れ方: 下の縁から上へ開き(clip-path)、中身は少し下から、写真は少し拡大した状態から戻る
function reveal(tile) {
  const tl = gsap
    .timeline({ paused: true })
    .fromTo(tile, { clipPath: "inset(100% 0% 0% 0%)" }, { clipPath: "inset(0% 0% 0% 0%)", duration: 0.9, ease: "power3.out" })
    .from(tile.querySelector(".tile__body"), { y: 32, duration: 0.9, ease: "power3.out" }, 0.08);
  const image = tile.querySelector(".tile__image");
  if (image) tl.from(image, { scale: 1.18, duration: 1.2, ease: "power3.out" }, 0);
  return tl;
}

document.querySelectorAll(".bento").forEach((section) => {
  // 行末の約物(「、」「。」)は数えず、その手前までの字面の左右の端の中央を行の中央にそろえる。
  // 約物は右へぶら下がる(含めて中央にすると、字が左へ寄って見える)。ずれは em で持ち、字の大きさが変わっても合う
  const title = section.querySelector(".bento__title");
  document.fonts.ready.then(() => {
    const style = getComputedStyle(title), ruler = document.createElement("canvas").getContext("2d");
    Object.assign(ruler, { font: `${style.fontWeight} ${style.fontSize} ${style.fontFamily}`, letterSpacing: style.letterSpacing, textAlign: "left", lang: document.documentElement.lang || "ja" });
    title.querySelectorAll("span").forEach((line) => {
      const { width } = ruler.measureText(line.textContent), ink = ruler.measureText(line.textContent.replace(/[、。,.」』)]+$/u, ""));
      line.style.translate = `${(width / 2 - (ink.actualBoundingBoxRight - ink.actualBoundingBoxLeft) / 2) / parseFloat(style.fontSize)}em`;
    });
  });
  // 動きを減らす設定では何もしない(タイルは最初から表示されている)
  if (reduceMotion) return;
  // 隠すのは clip-path だけ。表示される前のタイルの文も、スクリーンリーダーには届く
  const reveals = new Map([...section.querySelectorAll(".tile")].map((tile) => [tile, reveal(tile)]));

  // 表示するのは 1 回だけなので、スクロールを追い続ける ScrollTrigger は要らない。
  // 下を 15% 縮めた画面にタイルが入ったら(上端が下から 15% の線を越えたら)コールバックが呼ばれる。
  // 同じときに越えたタイルは 1 回のコールバックにまとまって届くので、その順番でずらして表示する
  const io = new IntersectionObserver(
    (entries) => {
      entries
        .filter((entry) => entry.isIntersecting)
        .forEach((entry, i) => {
          io.unobserve(entry.target);
          gsap.delayedCall(i * STAGGER, () => reveals.get(entry.target).play());
        });
    },
    { rootMargin: "0px 0px -15% 0px" }
  );
  reveals.forEach((_, tile) => io.observe(tile));
});

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

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