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

セクション

スクロールでセクションの大きな文字が左から塗られていく

デモ

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

仕様

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

しくみと調整

大きな文字が、スクロールに合わせて上の行から順に、左から右へ塗られていきます。スクロールを止めると、塗りは行の途中でもそのまま止まります。途中で止められるのは、文字ごとに色を変えるのではなく、行ごとに背景を伸ばす方式にしたからです。

SplitText で文章を行に分け、幅を文字に合わせた各行に、塗る色のグラデーションを背景として描いて、background-clip の text で文字の形に切り抜きます。その背景の幅を0%から100%へ、イージングなしで行ごとに順に伸ばします。範囲は、文章の上端が画面の高さの80%に来たときから下端が45%に来たときまでで、ScrollTrigger でスクロール量にそのまま連動させています(scrub)。なお background の一括指定は切り抜きの指定を初期値に戻すので、切り抜きは一括指定より後に書いています。

和文には語を区切る空白がないので、そのままでは文章全体がひとかたまりの行になります。文節の切れ目に <br> を入れて行を決めました。改行の文字や white-space の指定に頼ると、ひとまとまりのままです。狭い画面では <br> で区切った行がさらに折り返すので、Intl.Segmenter で文節の頭に幅のない空白を挟み、折り返したあとの見た目の行ごとに塗られるようにしています。行の幅は字形で決まるので、行に分けるのは書体の読み込みを待ってからにしています。

コード

HTML
<section class="fill-section">
  <p class="fill-eyebrow">Approach</p>

  <!--
    和文は空白で語に分かれないので、文節の切れ目に <br> を入れて行を決める。
    SplitText は <br> で行を分ける(英文なら <br> は要らず、幅で折り返した行に分かれる)。
  -->
  <p class="fill-text">気づかないほど自然に、<br>
    でも、たしかに伝わる。<br>
    そんな動きを、ひとつずつ<br>
    ていねいにつくっています。</p>
</section>

CSS
.fill-section {
  --dim: #343433; /* 塗る前の文字の色(背景色に文字の色を少し混ぜた暗いグレー) */
  --fill: #eeece6; /* 塗った後の文字の色 */
  padding: 40vh 6vw 22vh;
  background-color: #111111;
}

.fill-eyebrow {
  margin: 0 0 24px;
  font-size: 13px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #a8a6a0;
}

/* JS が動かないときは、ふつうの読める文字のまま(切り抜きは分割した後にだけ効かせる) */
.fill-text {
  margin: 0;
  font-size: clamp(28px, 4.4vw, 56px);
  font-weight: 700;
  line-height: 1.5;
  letter-spacing: 0.04em;
  color: var(--fill);
}

/* SplitText が作る行。塗る範囲を文字の幅にそろえ、背景を文字の形で切り抜く */
.fill-text.is-split .line {
  width: fit-content;
  color: transparent;
  background: linear-gradient(var(--fill), var(--fill)) 0 0 / 0% 100% no-repeat, var(--dim);
  -webkit-background-clip: text;
  /* 一括指定(background)は切り抜きを初期値に戻すので、必ずその後に書く */
  background-clip: text;
}

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

gsap.registerPlugin(ScrollTrigger, 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(() => {
  document.querySelectorAll(".fill-text").forEach((text) => {
    // 和文は空白で語に分かれないので、文節の頭に幅のない空白を挟んで分ける。
    // 狭い画面で <br> の行がさらに折り返しても、見た目の行ごとに塗れる
    const split = SplitText.create(text, {
      type: "lines",
      linesClass: "line",
      prepareText: (t) => Array.from(words.segment(t)).reduce((out, s) => out + (out && !joins(out, s) ? "\u200b" : "") + s.segment, ""),
      wordDelimiter: { delimiter: /\u200b/, replaceWith: "\u200b" },
    });
    text.classList.add("is-split"); // ここで初めて、文字の形で切り抜く見た目にする

    // 動きを減らす設定では、塗り終わった状態で止めておく
    if (reduceMotion) {
      gsap.set(split.lines, { backgroundSize: "100% 100%" });
      return;
    }

    gsap.fromTo(
      split.lines,
      { backgroundSize: "0% 100%" },
      {
        backgroundSize: "100% 100%",
        ease: "none",
        duration: 1,
        stagger: 1, // 1 行ずつ順に塗る
        scrollTrigger: {
          trigger: text,
          start: "top 80%", // 文章の上端が画面の高さの 80% に来たら塗り始める
          end: "bottom 45%", // 下端が 45% に来たら塗り終わる
          scrub: true, // スクロール量にそのまま連動させる(数値の scrub は実時間で追うので使わない)
        },
      }
    );
  });
});

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

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