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

スクロールでセクションの見出しと段落が行ごとに現れる

デモ

仕様

動作
DOM
種類
レシピ
使うもの
SplitText
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

見出しと段落を SplitText で行に分け、行ごとに窓(はみ出しを隠す箱)を付けます。行を窓の高さだけ下へ沈めておき、ブロックが画面に入ったら、窓の下から 1 行ずつせり上げます。窓の外は見えないので、文字が行の下から湧いてくるように見えます。

せり上げは 0.9 秒の power4.out で、行は 0.08 秒ずつずらします。出だしが速く止まり際が長いので、行はすっと上がって静かに止まり、読み始めるまで待たせません。始めるのは、ブロックの上端が画面の上から 8 割の高さまで入ったときです。画面の下端で始めると、いちばん動く所が画面の外で終わってしまいます。

和文は空白で語に分かれないため、そのままでは段落全体がひとつの行になります。文や文節の切れ目に <br> を入れて、行を決めてください。改行の文字を white-space で活かすやり方では、行がまとまってしまいます。狭い画面で行がさらに折り返すときは、Intl.Segmenter で文節の頭に幅のない空白を挟んで語に分けておくと、見た目の行ごとに現れます。行の幅は字形で決まるので、分けるのは書体の読み込みが終わってからにします。

大きな英字の見出しは、窓を行の高さぴったりにすると、g や p の下が切れて別の字に読めます。和文の見出しでも、英字に差し替えたときに備えて同じ余白を足しておきます。窓の下に 0.2em の余白を足し、同じだけ負の余白で行の間隔を戻します。沈める量も、足した余白ごと窓の高さにします。

出すのは画面に入った 1 回だけです。スクロールの位置を追い続ける ScrollTrigger は使わず、画面に入ったことを知らせるブラウザの仕組み(IntersectionObserver)で足ります。動きを減らす設定では行に分けず、そのまま見せます。ずらす間隔を広げると、1 行ずつ語りかけるような間ができます。会社の考え方やサービスの説明など、読ませたい節の見出しとリード文に向いています。

コード

HTML
<section class="lines">
  <h2 class="lines__title lines-reveal">動きには、<br>理由がある。</h2>

  <!--
    和文は空白で語に分かれないので、文や文節の切れ目に <br> を入れて行を決める。
    SplitText は <br> で行を分ける(英文なら <br> は要らず、幅で折り返した行に分かれる)。
  -->
  <p class="lines__text lines-reveal">Webサイトの動きは、見た目を飾るだけでなく、<br>
    いまどこにいて、次に何が起こるのかを<br>
    伝える役目を持っています。<br>
    だから、短く、わかりやすく。</p>

  <p class="lines__text lines-reveal">私たちは、ひとつひとつの動きに<br>
    理由を持たせてデザインしています。</p>
</section>

CSS
.lines {
  --ink: #262626;
  --ground: #f2f1ed;
  --measure: 720px; /* 本文の幅 */
  padding: 120px max(24px, (100% - var(--measure)) / 2);
  background-color: var(--ground);
  color: var(--ink);
}

.lines__title {
  margin: 0 0 32px;
  font-size: clamp(40px, 6vw, 64px);
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

.lines__text {
  margin: 0 0 24px;
  font-size: 18px;
  line-height: 1.8;
  letter-spacing: 0.04em;
}

JavaScript
import { gsap } from "gsap";
import { SplitText } from "gsap/SplitText";

gsap.registerPlugin(SplitText);

const words = new Intl.Segmenter("ja", { granularity: "word" });
// 前の語に付ける区切り: 句読点・1 字の助詞・ひらがなで終わる語に続くひらがな(「あり|ます」を 1 つにする)
const joins = (before, { segment, isWordLike }) =>
  !isWordLike || /^[ぁ-ん]$/.test(segment) || (/^[ぁ-ん]+$/.test(segment) && /[ぁ-ん]$/.test(before));
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

// 行の幅は字形で決まるので、書体の読み込みが終わってから行に分ける
document.fonts.ready.then(() => {
  // 動きを減らす設定では、分けずにそのまま見せる
  if (reduceMotion) return;

  document.querySelectorAll(".lines-reveal").forEach((block) => {
    // mask: "lines" で、行ごとにはみ出しを隠す窓が付く。和文は文節の頭に幅のない空白を挟んで分け、
    // 狭い画面で <br> の行がさらに折り返しても、見た目の行ごとに現れるようにする
    const split = SplitText.create(block, {
      type: "lines",
      mask: "lines",
      prepareText: (t) => Array.from(words.segment(t)).reduce((out, s) => out + (out && !joins(out, s) ? "\u200b" : "") + s.segment, ""),
      wordDelimiter: { delimiter: /\u200b/, replaceWith: "\u200b" },
    });
    // 大きな見出しは、窓の下に余白を足して g・p・y の下を切らない(行の間隔は負の margin で保つ)
    if (block.matches("h1, h2")) gsap.set(split.masks, { paddingBottom: "0.2em", marginBottom: "-0.2em" });

    const rise = gsap.fromTo(
      split.lines,
      { y: (i, line) => line.parentElement.offsetHeight }, // 窓の高さだけ沈めておく
      { y: 0, duration: 0.9, ease: "power4.out", stagger: 0.08, paused: true }
    );

    // ブロックの上端が、画面の下から 2 割の所まで入ったら 1 回だけ流す
    const io = new IntersectionObserver(
      ([entry]) => {
        if (!entry.isIntersecting) return;
        io.disconnect();
        rise.play();
      },
      { rootMargin: "0px 0px -20%" }
    );
    io.observe(block);

    // キーボードで中のリンクへ移ったときは、線まで入るのを待たずに出す
    block.addEventListener("focusin", () => rise.play());
  });
});

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

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