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

縦書きの見出しと本文が、スクロールに合わせて右の行から順に現れる

デモ

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

仕様

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

しくみと調整

縦書きで組んだ見出しと本文を、スクロールに合わせて右の行から 1 行ずつ出します。行の塊は position: sticky で画面に留め、そのあいだのスクロール量を、行を出す進み具合にそのまま割り当てます(scrub)。行の中では、上から下へ縁のやわらかいマスクが降りていくので、字が読む順に上から現れます。

各行には、同じ字を 2 層重ねています。上の層はふつうの文字で、下の層は字を透明にして、ぼかした text-shadow だけで描いた「にじみ」です。にじみの層は、文字の層より少し先を細い帯のマスクで通るので、墨がにじんでから字がくっきり出てくるように見えます。どちらのマスクも 0〜1 の CSS 変数 --p ひとつから計算し、GSAP はこの変数だけを動かします。出きると帯は行の外へ抜けるので、にじみは残りません。

句読点と括弧は、縦書きにするだけで和文の書体の縦書き用の字形になります(、と。は字の右上、「」は向きを変えた形)。字形を切り替える書体の機能を、自分で指定する必要はありません。2 桁の数字は text-combine-upright で 1 字分に収めます(縦中横)。行の頭の「は、既定では前に半字の空きが残って下がって見えるので、text-spacing-trim を trim-start にして行の頭をそろえます。行は自分で分け、文節の切れ目で改行します。

行は、同じ段落の次の行を 0.7、段落の頭の行を 1.3 の間隔で並べ、1 行を 1.2 の長さで出します(どれもスクロール量に対する割合)。行どうしが重なりながら出るので、右の行が出きる前に左の行が始まり、読む速さに合わせて文が流れます。塊を留めておく間は、節の高さを画面の 3 倍にして作ります。幅 680px 以下では縦の列が収まらないので横書きに切り替え、同じマスクを左から右へ向けて使います。

横書きの行を窓の下からせり上げる出し方や、行を左から塗っていく出し方と違い、縦書きの行を右から左へ、字の並ぶ向きに沿って出していきます。つまみは、マスクの縁のやわらかさ(行の長さの 30%)、にじみの影のぼかしと濃さ、行の間隔、節の高さです。動きを減らす設定では、画面に留めずに最初から出ている文として見せます。会社の思いや代表のあいさつ、随筆、和の商品の紹介に向いています。

コード

HTML
<!-- 縦書きの 1 行 = .vcopy__line。行は自分で分ける(和文は文節の切れ目で)。2 桁の数字は .vcopy__tcy で縦中横にする -->
<section class="vcopy" aria-labelledby="vcopy-title">
  <div class="vcopy__stage">
    <div class="vcopy__text">
      <p class="vcopy__eyebrow">Philosophy</p>
      <h2 class="vcopy__title" id="vcopy-title">
        <span class="vcopy__line">ちいさな仕事を、</span>
        <span class="vcopy__line">ていねいに重ねる。</span>
      </h2>
      <p class="vcopy__body">
        <span class="vcopy__line">私たちは、町の小さな工房から</span>
        <span class="vcopy__line">始まりました。創業から<span class="vcopy__tcy">20</span>年、</span>
        <span class="vcopy__line">看板も広告もほとんど出さずに、</span>
        <span class="vcopy__line">紹介と口伝えだけで続いてきました。</span>
        <span class="vcopy__line">「いい仕事は、次の仕事を呼ぶ」と</span>
        <span class="vcopy__line">先代はよく話していました。</span>
        <span class="vcopy__line">目の前の一人に、きちんと応える。</span>
        <span class="vcopy__line">これからも、急がず、手を抜かずに。</span>
      </p>
      <p class="vcopy__sign"><span class="vcopy__line">代表 木村 直樹</span></p>
    </div>
  </div>
</section>

CSS
.vcopy {
  --ink: #262626;
  --mute-small: #6b6b6b;
  --bleed: rgba(38, 38, 38, 0.55);   /* にじみの層の墨(文字の色をぼかした影) */
  --dir: 180deg;                     /* 行を出す向き(縦書きは上から下) */
  height: 300vh;                     /* このスクロールの間、行の塊を画面に留めて出していく */
  background: #f2f1ed;
  color: var(--ink);
}

.vcopy__stage {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: clip;
}

/* 縦書き。句読点と括弧は、和文の書体の縦書き用の字形になる。行頭の「の前の半字の空きは詰める */
.vcopy__text {
  writing-mode: vertical-rl;
  text-orientation: mixed;
  height: 420px;
  white-space: nowrap;
  text-spacing-trim: trim-start;
}
.vcopy__eyebrow { margin: 0 0 0 40px; font-size: 13px; font-weight: 700; line-height: 1; letter-spacing: 0.14em; text-transform: uppercase; color: var(--mute-small); }
.vcopy__title { margin: 0 0 0 56px; font-size: 40px; font-weight: 700; line-height: 1.5; letter-spacing: 0.08em; }
.vcopy__body { margin: 0 0 0 40px; font-size: 20px; line-height: 2.1; letter-spacing: 0.12em; }
.vcopy__sign { margin: 0; font-size: 14px; line-height: 2; letter-spacing: 0.12em; text-align: end; color: var(--mute-small); }
.vcopy__tcy { text-combine-upright: all; }

/* 1 行 = 文字の層とにじみの層。どちらも --p(0〜1)から、縁のやわらかいマスクで描く */
.vcopy__line { --p: 1; position: relative; display: block; }
.vcopy__ink {
  display: block;
  -webkit-mask-image: linear-gradient(var(--dir), #000 calc(var(--p) * 130% - 30%), transparent calc(var(--p) * 130%));
  mask-image: linear-gradient(var(--dir), #000 calc(var(--p) * 130% - 30%), transparent calc(var(--p) * 130%));
}
/* にじみは、文字の層の少し先を帯で通る(出きると帯が行の外へ抜けて消える) */
.vcopy__blur {
  position: absolute;
  inset: 0;
  color: transparent;
  text-shadow: 0 0 5px var(--bleed);
  -webkit-mask-image: linear-gradient(var(--dir), transparent calc(var(--p) * 150% - 50%), #000 calc(var(--p) * 150% - 20%), transparent calc(var(--p) * 150%));
  mask-image: linear-gradient(var(--dir), transparent calc(var(--p) * 150% - 50%), #000 calc(var(--p) * 150% - 20%), transparent calc(var(--p) * 150%));
}

/* 狭い画面では縦書きの列が収まらないので、横書きにして左から右へ出す */
@media (max-width: 680px) {
  .vcopy { --dir: 90deg; }
  .vcopy__text { writing-mode: horizontal-tb; height: auto; padding: 0 16px; }
  .vcopy__eyebrow { margin: 0 0 16px; }
  .vcopy__title { margin: 0 0 24px; font-size: 28px; }
  .vcopy__body { margin: 0 0 24px; font-size: 16px; line-height: 1.9; letter-spacing: 0.06em; }
  .vcopy__sign { letter-spacing: 0.06em; }
}

/* 動きを減らす設定では、スクロールで留めずに、最初から出ている文として見せる */
@media (prefers-reduced-motion: reduce) {
  .vcopy { height: auto; }
  .vcopy__stage { position: static; height: auto; padding: 96px 0; }
}

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

gsap.registerPlugin(ScrollTrigger);

const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const STEP = 0.7; // 同じ段落の次の行までの間
const BREAK = 1.3; // 段落の頭の行までの間(見出し → 本文 → 署名のあいだに少し間を置く)
const LINE_TIME = 1.2; // 1 行を出しきるまでの長さ(どれもスクロール量に対する割合)

document.querySelectorAll(".vcopy").forEach((section) => {
  const lines = [...section.querySelectorAll(".vcopy__line")];

  // 行の中身を、文字の層と、同じ字をぼかした影で描くにじみの層に分ける(にじみの層は読み上げない)
  lines.forEach((line) => {
    const ink = document.createElement("span");
    ink.className = "vcopy__ink";
    ink.append(...line.childNodes);
    const bleed = ink.cloneNode(true);
    bleed.className = "vcopy__blur";
    bleed.setAttribute("aria-hidden", "true");
    line.append(ink, bleed);
  });

  // 動きを減らす設定では、最初から出ている(--p は CSS の既定の 1)
  if (reduceMotion) return;

  // 右の行(横書きでは上の行)から順に。塊が画面に留まっているあいだのスクロール量に、そのまま割り当てる
  const tl = gsap.timeline({
    scrollTrigger: { trigger: section, start: "top top", end: "bottom bottom", scrub: true },
  });
  let at = -STEP;
  lines.forEach((line) => {
    at += !line.previousElementSibling && at > 0 ? BREAK : STEP;
    tl.fromTo(line, { "--p": 0 }, { "--p": 1, duration: LINE_TIME, ease: "none" }, at);
  });
});

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

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