【CSS+JS】テキストを1文字ずつ表示するアニメーションの実装方法

はじめに

モダンなサイトでよく見かける「テキストを1文字ずつ表示するアニメーション」の実装方法を解説します。

実装

サンプル

繰り返し動作を確認したい場合は右下の「Rerun」ボタンをクリックしてください。

HTMLの実装

コンテナーの中に二行のテキストを入れています。行数は好きなだけ増やすことができます。

HTML

<div class="container">
  <div class="line">Lorem ipsum dolor sit amet, consectetur adipisicing elit,</div>
  <div class="line">sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</div>
 </div>

Slim

.container
  .line Lorem ipsum dolor sit amet, consectetur adipisicing elit,
  .line sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

CSSの実装

まずはCSSの全文を掲載します。

後述するJavaScriptの実装でテキストの文字一つひとつをspanタグで囲み、それらに対して順番にアニメーションディレイを設定します。このとき、Sassの@forによる繰り返し処理が必要なので通常のCSSではなくSassで記述してください。

SCSS

.container {
  .line {
    overflow: hidden;
    span {
      display: inline-block;
      transform: translateY(2rem);
      animation: slideText 1s forwards;
    }
  }
  .line:nth-of-type(1) {
    @for $i from 1 through 100 {
      span:nth-of-type(#{$i}) {
        animation-delay: #{$i * .02}s;
      }
    }
  }
  .line:nth-of-type(2) {
    @for $i from 1 through 100 {
      span:nth-of-type(#{$i}) {
        animation-delay: #{.25 + $i * .02}s;
      }
    }
  }
}

@keyframes slideText {
  100% { transform: translateY(0); }
}

一つひとつ分解して説明していきます。

SCSS

  .line {
    overflow: hidden;
  }

要素内容のはみ出し部分を非表示にします。

SCSS

    span {
      display: inline-block;
      transform: translateY(2rem);
      animation: slideText 1s forwards;
    }

@keyframes slideText {
  100% { transform: translateY(0); }
}

初期表示位置をY軸方向に2rem(2文字分)ずらします。transformはインライン要素(spanタグのデフォルト)には効かないのでインラインブロック要素にします。 slideTextという表示位置を戻すアニメーションを1秒かけて実行します。

SCSS

  .line:nth-of-type(1) {
    @for $i from 1 through 100 {
      span:nth-of-type(#{$i}) {
        animation-delay: #{$i * .02}s;
      }
    }
  }
  .line:nth-of-type(2) {
    @for $i from 1 through 100 {
      span:nth-of-type(#{$i}) {
        animation-delay: #{.25 + $i * .02}s;
      }
    }
  }

一行目と二行目でスタイルを分けています。Sassの@forを使ってspanタグに0.02秒ずつずらしてアニメーションディレイを設定します。一個目は0.02秒、二個目は0.04秒、三個目は0.06秒...という具合です。二行目は一行目より少しずらして開始させるため、さらに0.25秒足しています。

JavaScriptの実装

まずはJavaScriptの全文を掲載します。指定した要素のテキストを1文字ずつspanタグで囲む処理を実装します。

JavaScript

/**
 * @class SpanWrapText
 * @description テキストを1文字ずつspanで囲む
 * @argument target 対象のテキストを含む要素
 */
class SpanWrapText {
  constructor(target) {
    this.target = target;
    this.nodes = this.target.childNodes;
    this.convert();
  }

  /**
   * @function convert
   * @description テキストを1文字ずつspanで囲む
   */
  convert() {
    let spanWrapText = '';

    this.nodes.forEach((node) => {
      if (node.nodeType == 3) { // テキストの場合
        // 改行コードを削除
        const text = node.textContent.replace(/\r?\n/g, '');
        // spanタグで囲んで連結
        spanWrapText = spanWrapText + text.split('').reduce((accumulator, currentValue) => {
          currentValue = currentValue.replace(' ', '&nbsp;');
          return accumulator + `<span>${currentValue}</span>`;
        }, '');
      } else { // テキスト以外の場合
        // brなどの要素はそのまま連結
        spanWrapText = spanWrapText + node.outerHTML;
      }
    });

    this.target.innerHTML = spanWrapText;
  }
}

document.addEventListener('DOMContentLoaded', event => {
  document.querySelectorAll('.container .line').forEach(element => {
    new SpanWrapText(element);
  });
});

一つひとつ分解して説明していきます。

JavaScript

  constructor(target) {
    this.target = target;
    this.nodes = this.target.childNodes;
    this.convert();
  }

SpanWrapTextクラスのコンストラクターでは、対象のテキストを含む要素から子ノードを取得し、convertメソッドを呼び出します。

JavaScript

    this.nodes.forEach((node) => {
      if (node.nodeType == 3) { // テキストの場合
        // 改行コードを削除
        const text = node.textContent.replace(/\r?\n/g, '');
        // spanタグで囲んで連結
        spanWrapText = spanWrapText + text.split('').reduce((accumulator, currentValue) => {
          currentValue = currentValue.replace(' ', '&nbsp;');
          return accumulator + `<span>${currentValue}</span>`;
        }, '');
      } else { // テキスト以外の場合
        // brなどの要素はそのまま連結
        spanWrapText = spanWrapText + node.outerHTML;
      }
    });

    this.target.innerHTML = spanWrapText;

取得したノードの種類を判定し、テキストとそれ以外で処理を分けます。ノードの種類がテキストの場合は1文字ずつspanタグで囲み再連結します。ノードの種類がテキスト以外の場合は何もせずそのまま連結します。

ノードの種類については以下を参照してください。


JavaScript

document.addEventListener('DOMContentLoaded', event => {
  document.querySelectorAll('.container .line').forEach(element => {
    new SpanWrapText(element);
  });
});

最後に、対象のテキストを含むすべての行に対してSpanWrapTextクラスを呼び出します。

まとめ

複雑だと思われる「テキストを1文字ずつ表示するアニメーション」ですが、やっていることは1文字ずつspanタグで囲み少しずつずらしてアニメーションを実行しているだけです。JavaScriptの処理を見て「うわ、長い...」と思われるかもしれませんが、1/3くらいはコメントなので実際の処理はそれほど長くありません。

まずはコピペでも結構ですので、本記事を参考にして実装していただければと思います。

関連記事

【CSS+JS】メニューアイコンの一種、ベントーメニューの実装方法(アニメーション付き)
# はじめに メニューアイコンの中ではハンバーガーメニューが有名だと思いますが、その他にもいろいろな種類があって、それぞれに名前もつけられています。 <a class="gallery" data-group="gallery" href= [...]
2021年10月18日 14:26
【CSS+JS】メインコンテンツの裏から現れるフッターの実装方法
# はじめに オシャレなサイトなどでたまに見かける「メインコンテンツの裏から現れるフッター」の実装方法について説明します。 # サンプル <iframe height="392" style="width: 100%;" scrollin [...]
2021年10月18日 13:26
【CSS+JS】背景画像の視差効果(パララックス)を実装する方法
# はじめに JavaScriptプラグインを使わずに、背景画像の視差効果(パララックス)をVanilla JS(ピュアなJavaScript)だけで実装する方法について説明します。 # サンプル まず、背景画像の視差効果(パララックス) [...]
2021年10月17日 12:08
【CSS+JS】ウィンドウ内全体にファイルをドラッグ&ドロップしてアップロード
# はじめに ユーザーが選択したファイルをアップロードする必要がある場合、ファイル選択フィールドを設置することはもちろんですが、ファイルをドラッグ&ドロップしてアップロードできるようにもなっていると使い勝手のいいサービスだと言えます。しかし、ドラ [...]
2021年6月2日 15:15
【CSS+JS】モーダルウィンドウを表示しその中にYouTube動画を動的に埋め込む
# はじめに 動画のサムネイル画像をクリックしてモーダルウィンドウを表示し、その中にYouTube動画を動的に埋め込む方法について説明します。なお、本記事ではYouTube動画を動的に埋め込むまでを範囲とし、埋め込んだ動画の再生制御などは範囲外と [...]
2021年5月20日 23:23
【CSS】CSSだけでMarkdownのコードにファイル名をつける
# はじめに Markdownのコードに、そのコードがどのファイルのものなのかを示すためにファイル名が書いてあるとわかりやすいです。以下はQiitaの記事でコードを書いたときの一例です。 <img data-src="https://i.i [...]
2021年4月6日 14:39
【CSS】カーソルを乗せると流れるようなアニメーションのハンバーガーメニューを作る
# はじめに 今回はハンバーガーメニューにカーソルを乗せると川のように流れるアニメーションを作りたいと思います。 今回の記事は以下の記事をベースにしていますので、まだご覧になっていない方は先にこちらをご覧ください。 <div clas [...]
2020年8月31日 8:54
【CSS】JavaScriptを使わずにハンバーガーメニューを作る
# はじめに 当初はスペースの限られるスマートフォンなどの低解像度デバイス向けサイトにおいてスペースを有効活用するために登場したハンバーガーメニューですが、最近はPCなどの高解像度デバイス向けサイトでも使われているのをよく見かけます。 何よ [...]
2020年8月31日 7:34