テキストアニメーションとは

文字を 1 字ずつ順番に表示していく演出。span に分割して順に現れさせる方法と、タイプライター風に打ち込む方法がある。

概要

見出しや一文を、まとめて表示するのではなく 1 文字ずつ順番に表示していく演出です。 文字を span で 1 字ずつ分割し、animation-delay を少しずつずらして次々に現れさせる方法と、幅を広げながら文字を打ち込んでいくように見せる「タイプライター」風の方法(steps() を使う)の 2 通りがよく使われます。

別名

表記ゆれ:テキストアニメーション、文字送り、タイプライター、1 文字ずつ表示

AI への伝え方

「見出しを 1 文字ずつ順番に表示して」「タイプライターみたいに文字が打ち込まれるようにして」のように、見せ方の種類を伝えると実装方法が決まります。 文字数が多いと分割処理が重くなりやすいため、長い文章向きではなく短い見出しやキャッチコピー向きの演出だと伝えておくと安全です。

似たパーツとの違い

「フェードイン」は要素全体が一度にふわっと現れる演出で、テキストアニメーションはそれを文字単位まで細かく分解した演出という関係です。

デモ

1 文字ずつ表示

ようこそ

タイプライター風

Hello, world.

コード

HTML
<div class="textanim-demo">
  <p class="textanim-demo__label">1 文字ずつ表示</p>
  <p class="textanim-demo__chars" data-textanim-chars>ようこそ</p>

  <p class="textanim-demo__label">タイプライター風</p>
  <p class="textanim-demo__typewriter" data-textanim-typewriter>Hello, world.</p>

  <button class="textanim-demo__replay" type="button" data-textanim-replay>もう一度再生</button>
</div>
CSS
.textanim-demo {
  padding: 24px;
  font-family: sans-serif;
  color: #333333;
}

.textanim-demo__label {
  margin: 16px 0 8px;
  font-size: 12px;
  color: #666666;
}

.textanim-demo__chars {
  margin: 0;
  font-size: 22px;
  font-weight: 700;
}

.textanim-demo__chars span {
  display: inline-block;
  opacity: 0;
  transform: translateY(8px);
  animation: textanim-demo-char 0.4s ease forwards;
  animation-delay: calc(var(--i) * 0.08s);
}

@keyframes textanim-demo-char {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.textanim-demo__typewriter {
  display: inline-block;
  margin: 0;
  overflow: hidden;
  white-space: nowrap;
  border-right: 2px solid #0017c1;
  font-size: 20px;
  font-family: monospace;
  /* 文字数(13 文字)ぶんの steps でカクカクと打ち込むように見せる */
  animation: textanim-demo-type 1.4s steps(13, end) forwards, textanim-demo-caret 0.8s step-end infinite;
}

@keyframes textanim-demo-type {
  from {
    width: 0;
  }
  to {
    width: 13ch;
  }
}

@keyframes textanim-demo-caret {
  50% {
    border-color: transparent;
  }
}

.textanim-demo__replay {
  display: block;
  margin-top: 20px;
  padding: 8px 16px;
  border: 1px solid #0017c1;
  border-radius: 6px;
  background: #ffffff;
  color: #0017c1;
  font-size: 13px;
  cursor: pointer;
}

@media (prefers-reduced-motion: reduce) {
  .textanim-demo__chars span {
    animation: none;
    opacity: 1;
    transform: none;
  }

  .textanim-demo__typewriter {
    animation: none;
    width: 13ch;
    border-right: 0;
  }
}
JS
const charsEl = document.querySelector("[data-textanim-chars]");
const typewriterEl = document.querySelector("[data-textanim-typewriter]");
const replayButton = document.querySelector("[data-textanim-replay]");

function splitIntoChars(el) {
  const text = el.textContent ?? "";
  el.innerHTML = "";
  [...text].forEach((char, index) => {
    const span = document.createElement("span");
    span.textContent = char;
    span.style.setProperty("--i", String(index));
    el.appendChild(span);
  });
}

function replay() {
  if (charsEl) splitIntoChars(charsEl);
  if (typewriterEl) {
    // アニメーションをいったん外して再度当て直すことで、最初から再生させる
    typewriterEl.style.animation = "none";
    void typewriterEl.offsetWidth;
    typewriterEl.style.animation = "";
  }
}

replay();
replayButton?.addEventListener("click", replay);