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

見出しをタイプライター風に打ち出し、カーソルが点滅する

デモ

仕様

用途
ヒーロー
動作
DOM
種類
レシピ
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

見出しを1文字ずつ打ち出す、画面に入ったときに一度だけ動く演出です。文字数を0から文の長さまで数えるだけの値をトゥイーンし、その数だけ文を切り出して表示しています。完成した文は透明のまま置いて幅を確保し、打ち出す行をその上に重ねています。

1文字あたり0.16秒の速さで打ちます。和文は1字が欧文の倍ほど広いので、欧文と同じ間隔で打つとカーソルが走るように見えます。カーソルの点滅はCSSのステップ式のアニメーションで表しているので、打っている間だけクラスを外して点灯させたままにしています。

和文のかなと漢字は全角でどれも同じ幅なので、等幅の書体でなくても、カーソルが1打ごとに同じ幅だけ進みます。ただし書体によってはかなの組み合わせで字を詰めるので、font-kerningで詰めを切っています。詰めが残ると、カーソルの進む間隔が字ごとにばらつき、リズムが読み取りにくくなります。読み上げには完成した文をそのまま渡し、打ち出す行とカーソルは読み上げから外しています。

つまみは打つ速さと書体です。欧文の見出しを打つなら、等幅の書体にするとリズムがそろいます。ヒーローのキャッチコピーや、開発者向けサイトのコード風の見出しに向いています。

コード

HTML
<section class="typewriter">
  <h2 class="typewriter__heading" aria-label="長く愛されるものを、つくろう。">
    <span class="typewriter__ghost" aria-hidden="true">長く愛されるものを、つくろう。</span>
    <span class="typewriter__line" aria-hidden="true">
      <span class="typewriter__typed"></span><span class="typewriter__cursor"></span>
    </span>
  </h2>
</section>

CSS
.typewriter {
  display: grid;
  place-items: center;
  min-height: 60vh;
  padding: 96px 6vw;
  background: #111111;
  color: #eeece6;
}

.typewriter__heading {
  position: relative;
  margin: 0;
  max-width: 18em;
  font-size: clamp(20px, 5vw, 48px);
  font-weight: 800;
  line-height: 1.4;
  letter-spacing: -0.02em;
  /* 和文の全角の字はどれも同じ幅。字詰めだけ切ると、1 打ごとにカーソルが同じ幅だけ進む */
  font-kerning: none;
}

/* 完成した文を透明のまま置き、折り返す位置ごと見出しの幅を確保する */
.typewriter__ghost {
  visibility: hidden;
}

/* 打ち出す行は、幅を確保した見出しに同じ幅で重ね、折り返す位置をそろえる */
.typewriter__line {
  position: absolute;
  inset: 0;
}

.typewriter__cursor {
  display: inline-block;
  width: 0.09em;
  height: 1.08em;
  margin-left: 0.06em;
  /* 幅を確保した見出しにはカーソルの分が無いので、右の余白で打ち消して、最後の字の後ろで次の行へ落ちないようにする */
  margin-right: -0.15em;
  vertical-align: -0.16em;
  background: #eeece6;
  animation: typewriter-blink 1s steps(1) infinite;
}

/* 打っている・消しているあいだは点灯したまま。止まっている間だけ点滅する */
.typewriter.is-typing .typewriter__cursor {
  animation: none;
}

@keyframes typewriter-blink {
  50% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .typewriter__cursor { animation: none; }
}

JavaScript
import { gsap } from "gsap";

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

// 字形が確定してから打ち始める(書体が差し替わると、確保した幅と折り返す位置が変わる)
document.fonts.ready.then(() => {
  document.querySelectorAll(".typewriter").forEach((root) => {
    const typed = root.querySelector(".typewriter__typed");
    const text = root.querySelector(".typewriter__ghost").textContent;

    if (reduceMotion) {
      typed.textContent = text;
      return;
    }

    const type = () => {
      root.classList.add("is-typing");
      const state = { n: 0 };
      gsap.to(state, {
        n: text.length,
        duration: text.length * 0.16, // 1 字 0.16 秒。和文は 1 字が欧文の倍ほど広いので、欧文より間をあける
        ease: "none",
        onUpdate: () => (typed.textContent = text.slice(0, Math.round(state.n))),
        onComplete: () => root.classList.remove("is-typing"),
      });
    };

    // 画面に入ったら 1 回だけ打つ
    const io = new IntersectionObserver(
      (entries, self) => {
        for (const entry of entries) {
          if (!entry.isIntersecting) continue;
          self.unobserve(entry.target);
          type();
        }
      },
      { rootMargin: "0px 0px -15%" }
    );
    io.observe(root);
  });
});

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

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