22
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

CSSのテキストアニメーションサンプル

Last updated at Posted at 2022-12-20

こちらの記事は「Relic Advent Calendar 2022」の17日目の記事です!

とりあえずサンプルはこちら

See the Pen CSS text animation by altn (@_sleeepin) on CodePen.

はじめまして、動きのあるwebサイトが好きな者です。
この記事では、見出しやメインビジュアルで使えるCSSのテキストアニメーションの紹介をしていきたいと思います。
「CSSでここまで動かせるんだ!」「こういうプロパティの使い方があるんだ!」とCSSアニメーションの面白さ・奥深さを少しでも感じていただけたら嬉しいです。

想定読者:
・CSSアニメーション初心者
・アニメーションの引き出しを増やしたいと思っている方
・ギャラリーサイトに載っているようなカッコいいサイトを作ってみたいと思っている方

この記事で書かないこと

  • htmlの構造について(サンプルのcodepenをみてね)
  • 各CSSプロパティの説明(MDNを読んでね)
  • javascriptの説明(最低限だけ使ってます)

前提条件

  • 一文字ずつ文字が出現するアニメーションは、javascriptで一文字ずつ分割して span でくくっています。意外と力技です。
  • 各ブロック内のテキストが画面内に入ったら is-active というアニメーション発火用のクラスを付与しています。
  • windows/chromeでのみ表示確認を行っていますので、業務で使用する際は実機チェックを十分に行ってください。
  • あくまでアニメーションを紹介するためのものなので、アクセシビリティ対応はこちらのサンプルでは行っていません。

アクセシビリティについて

  • テキストを一文字ずつ span でくくるような実装はアクセシビリティ的にはよろしくないようです。具体的な対応方法についてはこちらの記事がわかりやすかったので紹介させていただきます。
  • 今回紹介するアニメーションには文字が点滅するアニメーションも含まれています。ユーザーによっては発作を引き起こす演出になるため、利用には十分気を付けましょう。
  • prefers-reduced-motionという、余分なアニメーションを無効にしたいユーザー向けに用意されているメディアクエリも合わせて使ってみるといいかもしれません。

①下から一文字ずつでてくるやつ

css_anim_1.gif

scss ※長いので折り畳んでます
.text-split {
  overflow: hidden;
  span {
    // 一文字ずつspanで囲まれた要素
    transform: translateY(100%);
    .is-active & {
      transform: translateY(0);
      transition: transform cubic-bezier(0.7, 0.2, 0.1, 1) 0.8s;
      @for $i from 1 through 13 {
        &:nth-child(#{$i}) {
          // 若干時差をつけながら出現させる
          transition-delay: #{($i - 1) * 0.04s};
        }
      }
    }
  }
}

.text-split は Relic / CO-INNOVATION / COMPANY. それぞれのテキストを囲む要素につけているクラスです。(以降も同じ※一部例外あり)
overflow: hidden をかけた親要素の中で、spantransform: translateY で上下しているというシンプルな仕組みです。
下からではなく、上から降ってくる感じにしてもいい感じになります。

②若干角度をつけながら下から一文字ずつ出てくる

css_anim_2.gif

scss
.text-split {
  overflow: hidden;
  span {
    // 一文字ずつspanで囲まれた要素
    transform: translate(-80px, 70%) rotate3d(1, 0.3, 0, 90deg);
    transform-origin: 50% 100%;
    .is-active & {
      transform: translate(0, 0) rotate3d(0, 0, 0, 0);
      transition: transform 1.4s cubic-bezier(0.08, 0.8, 0.315, 1);
      @for $i from 1 through 13 {
        &:nth-child(#{$i}) {
          transition-delay: #{($i - 1) * 0.04s};
        }
      }
    }
  }
}

①とほぼ同じですが、若干斜めから入ってくるような形にするために rotate3d の記述を追加したり、translateX の値を調整しています。
また、transform-origin で変形させる要素の基準点をbottomに変更してみました。
三角やひし形など、斜めのモチーフを使ったサイトに合いそうだなと思います。
このような感じで transform は複数の値を設定できるため、少し値を追加してみるだけで雰囲気を変えることが出来ます。

③スライドイン

css_anim_3.gif

<div class="block block--3">
   <div class="title">
     <h2 class="text-item"><span>Relic</span></h2>
     <div class="text-item"><span>CO-INNOVATION</span></div>
     <div class="text-item"><span>COMPANY.</span></div>
   </div>
</div>

こちらは他のサンプルとは若干htmlの構造が異なります。overflow:hidden をかけた .text-item の中にさらにテキストを囲む span を追加しています。

scss
.text-item {
  width: fit-content;
  overflow: hidden;
  transform: translateX(-100%);
  > span {
    display: block;
    // 親要素と同じ分だけ逆方向に移動させる
    transform: translateX(100%);
  }
  .is-active & {
    // 全く同じduration, delayを両方に設定する
    transform: translateX(0);
    transition: transform cubic-bezier(0.74, 0, 0.24, 0.99) 1.1s;
    > span {
       transform: translateX(0);
       transition: transform cubic-bezier(0.74, 0, 0.24, 0.99) 1.1s;
    }
    @for $i from 1 through 3 {
      &:nth-child(#{$i}) {
        transition-delay: #{($i - 1) * 0.13s};
        > span {
          transition-delay: #{($i - 1) * 0.13s};
        }
      }
    }
  }
}

文章だとわかりづらいので、図でスライドインの原理を説明します。
テキストを囲む .text-itemtransform: translateX(-100%) をかけ要素分だけ移動させます。
text_anim_slide_1.png
                 ↓
text_anim_slide_2.png
そして中の span 要素を、親要素と同じ分だけ transform: translateX(100%) で逆方向に移動させ、元の位置に戻します。
text_anim_slide_3.png
.text-itemoverflow: hidden をかけることで、.text-item からはみ出た span は見えなくなります。
text_anim_slide_4.png
最後に全く同じアニメーションで元の位置に戻すと、スライドインしたようなアニメーションになります。
↓途中経過の様子
text_anim_slide_5.png
clip-path を使えば span タグなしでもっとシンプルに同じアニメーションを実現できますが、transform のほうが動きが軽いので今回はそうしています。

④横からフェードイン&グラデーション

css_anim_4.gif

scss
.text-split {
  span {
    // 文字色を青にする
    color: $blue;
    // 30px右からフェードイン
    opacity: 0;
    transform: translateX(30px);
    .is-active & {
      color: #fff;
      opacity: 1;
      transform: translateX(0);
      transition: opacity 1.2s cubic-bezier(0.16, 1, 0.3, 1),
        transform 1.2s cubic-bezier(0.16, 1, 0.3, 1),
        color 2s cubic-bezier(0.19, 1, 0.22, 1) .2s; // フェードインした後に文字色だけ遅れて変化させたいため、delayをつける
    }
  }
  .is-active & {
    @for $i from 1 through 3 {
      &:nth-child(#{$i}) {
        span {
         @for $j from 1 through 14 {
          &:nth-child(#{$j}) {
            // 一行ずつ時差をつけるための変数
            $delay1: ($i - 1) * 0.05s;
            // 上からopacity、transform、colorのdelay
            transition-delay:
                #{($j - 1) * 0.04s + $delay1},
                #{($j - 1) * 0.04s + $delay1},
                #{($j - 1) * 0.05s + .2s + $delay1};
            }
          } 
        }
      }
    }
  }
}

こちらは一文字ずつ右からフェードインさせ、少し遅れて文字色を青→白に変化させるアニメーションです。
あえて color が変化するタイミングを遅くしたり transition-duration を長めにすることで、余韻を感じられるようにしています。
最初の文字色をゴールドとかにしたら高級感が出そう。

⑤clip-path

css_anim_5.gif

<div class="block block--5">
  <div class="title">
    <div class="text-wrap" data-text="Relic">
      <h2 class="text-item">Relic</h2>
    </div>
    <div class="text-wrap" data-text="CO-INNOVATION">
      <p class="text-item">CO-INNOVATION</p>
    </div>
    <div class="text-wrap" data-text="COMPANY.">
      <p class="text-item">COMPANY.</p>
    </div>
  </div>
</div>

それぞれのテキストを囲む .text-wrapdata-text 属性にテキストを指定します。(疑似要素作成のため)

scss
.text-wrap {
  position: relative;
  width: fit-content;
  z-index: 1;
  &::before,
  &::after {
    // 同じテキストの疑似要素を作成し、元のテキストにぴったりと重ねる
    position: absolute;
    content: attr(data-text);
    top: 0;
    left: 0;
    width: 100%;
    z-index: -1;
    clip-path: polygon(0 0, 0 0, -5% 100%, 0% 100%);
  }
  &::before {
    color: $yellow;
  }
  &::after {
    color: #000;
  }
  .is-active & {
    &::before,
    &::after {
      // アニメーション終了後非表示にする必要があるため、opacityを0にする
      opacity: 0;
      clip-path: polygon(0 0, 105% 0, 100% 100%, 0% 100%);
      transition: clip-path 1s cubic-bezier(0.42, 0.06, 0.1, 1), opacity 0s;
    }
    @for $i from 1 through 3 {
      &:nth-child(#{$i}) {
        &::before {
          // clip-pathのdelay、opacityのdelay
          transition-delay: #{($i - 1) * .12s}, #{($i - 1) * .12s + 1s};
        }
        &::after {
          // clip-pathのdelay、opacityのdelay
          transition-delay: #{($i - 1) * .12s + .1s}, #{($i - 1) * .12s + 1.1s};
        }
        .text-item {
          transition-delay: #{($i - 1) * .12s + .25s};
        }
      }
    }
  }
}

// 最終的に表示する要素
.text-item {
  color: $blue;
  clip-path: polygon(0 0, 0 0, -5% 100%, 0% 100%);
  .is-active & {
     clip-path: polygon(0 0, 105% 0, 100% 100%, 0% 100%);
     transition: clip-path 1s cubic-bezier(0.42, 0.06, 0.1, 1) 0.25s;
  }
}
  • 疑似要素で作った2つのテキスト要素を、absoluteで元のテキストと同じ位置に重ねます。(この際、最後に表示される要素が重なり順的に一番上に来る必要があります)
  • それぞれのテキストの文字色を変えます。
  • clip-path で斜めにスライドインするアニメーションを作成します。
  • transition-delay でそれぞれ少し遅れてアニメーションさせます。

⑥clip-path & グラデーション

css_anim_6.gif

scss
.text-split {
  span {
     color: red;
     // clip-pathパターン①
     clip-path: polygon(0 50%, 100% 50%, 100% 50%, 0 50%);
     &:nth-of-type(3n+2) {
       // clip-pathパターン②
       clip-path: polygon(0 0, 1% 0, 1% 100%, 0 0);
     }
     &:nth-of-type(3n+3) {
       // clip-pathパターン③
       clip-path: polygon(0 100%, 100% 100%, 100% 100%, 0 100%);
     }
    .is-active & {
      color: #fff;
      clip-path: polygon(100% 0, 100% 100%, 0 100%, 0 0);
      transition:
        clip-path 0.65s cubic-bezier(0.165, 0.84, 0.44, 1),
        color 1s cubic-bezier(0.19, 1, 0.22, 1);
    }
  }
  .is-active & {
    @for $i from 1 through 3 {
      span {
        @for $j from 1 through 20 {
          &:nth-child(#{$j}) {
            $delay1: ($i - 1) * 0.05s;
            transition-delay:
                #{($j - 1) * 0.03s + $delay1},
                #{($j - 1) * 0.03s + .2s + $delay1};
          }
        }
      }
    }
  }
}
  • ランダムっぽいアニメーションにするため、3種類の clip-path を用意して隣接する文字と被らないように指定します。
  • clip-path のアニメーション後、少し遅れて文字色を赤→白に変化させます。
  • 「自分で未来を切り拓け。」みたいなかっこいいキャッチに似合いそうだなと思いながら作りました。

⑦テキストが真ん中から分割された状態で出現

css_anim_7.gif

<div class="block block--7">
  <div class="title">
    <h2 class="text-item">
      Relic
      <span class="text-half text-half--top" data-text="Relic"></span>
      <span class="text-half text-half--btm" data-text="Relic"></span>
    </h2>
    <div class="text-item">
      CO-INNOVATION
      <span class="text-half text-half--top" data-text="CO-INNOVATION"></span>
      <span class="text-half text-half--btm" data-text="CO-INNOVATION"></span>
    </div>
    <div class="text-item">
      COMPANY.
      <span class="text-half text-half--top" data-text="COMPANY."></span>
      <span class="text-half text-half--btm" data-text="COMPANY."></span>
    </div>
  </div>
</div>
scss
.text-item {
  position: relative;
  color: transparent;
  .text-half {
    display: block;
    position: absolute;
    left: 0;
    width: 100%;
    height: 50%; // 高さを半分にし、上下に配置
    overflow: hidden;
    &::before {
      position: absolute;
      content: attr(data-text);
      left: 0;
      white-space: no-wrap;
      color: #fff;
    }
    &--top {
      top: 0;
      &::before {
        top: 0;
        transform: translateY(100%);
      }
    }
    &--btm {
      bottom: 0;
      &::before {
        bottom: 0;
        transform: translateY(-100%);
      }
    }
  }
  .is-active & {
    .text-half {
      &::before {
        transform: translateY(0);
        transition: 1s cubic-bezier(0.25, 1, 0.5, 1) .1s;
      }
    }
  }
}
  • .text-itemの50%の高さの span 要素を二つ作成し、absoluteで上下に配置します。
  • spanoverflow: hidden をかけ、半分しか見えないようにします。
  • span の疑似要素でテキストを作成し、テキストの上下半分ずつ表示
  • あとは span の中で transform: translateY() でアニメーションさせると真ん中の線からにゅっと出現したかのような演出になります。

⑧ランダムな順番で一文字ずつフェードイン

css_anim_8.gif

scss
.text-split {
  span {
    opacity: 0;
    .is-active & {
      opacity: 1;
      transition: opacity 0.4s cubic-bezier(0.12, 0, 0.39, 0);
      @for $i from 1 through 7 {
        &[data-random="#{$i}"] {
          transition-delay: #{($i - 1) * 0.03s};
        }
      }
    }
  }
}
  • テキストを一文字ずつjsで分割する際、data-random 属性に 1~7 のランダムな数字を割り当てるようにします。
text.forEach((t, index) => {
    newText += `<span data-random="${getRandomInt(1, 8)}">${t}</span>`;
});
  • テキストをフェードインで表示させますが、 transition-delay のタイミングを data-random の値によって設定することで、出現タイミングをランダムにしています。
@for $i from 1 through 7 {
  &[data-random="#{$i}"] {
    transition-delay: #{($i - 1) * 0.03s};
  }
}
  • ランダムにすることで、ただのフェードインとはまた違った印象になるかと思います。

⑨デジタルチックな点滅アニメーション

css_anim_9.gif

scss
.text-split {
  span {
    opacity: 0;
    .is-active & {
      animation: flash 0.1s 2 cubic-bezier(0.47, 0, 0.745, 0.715) forwards normal 0.2s;
      @for $i from 1 through 14 {
        &:nth-child(#{$i}) {
          animation-delay: #{($i - 1) * 0.025s + .1s};
        }
      }
    }
  }
}

@keyframes flash {
  0% {
    opacity: 1;
  }
  50% {
    opacity: 0.4;
  }
  100% {
    opacity: 1;
  }
}
  • flashという点滅用のkeyframeアニメーションを作成し、2回繰り返し再生しています。
  • SFとかデジタルっぽいサイトに合いそう

⑩点滅アニメーション2

css_anim_10.gif

scss
.text-split {
  span {
     opacity: 0;
     text-stroke: 1 #fff;
    .is-active & {
      animation: text-blink backwards 0.34s steps(1);
      opacity: 1;
      // nth-childでdelayを設定しつつ、data-randomの値によっては順番が多少前後する
      @for $i from 1 through 14 {
        @for $j from 1 through 7 {
          &:nth-child(#{$i})[data-random='#{$j}'] {
            animation-delay: #{($i - 1) * 0.02s + ($j * 0.02s)};
          }
        }
      }
    }
  }
}

@keyframes text-blink {
  0% {
    -webkit-text-stroke-width: 0;
    opacity: 0;
    color: #fff;
  }
  12.5% {
    -webkit-text-stroke-width: 0;
    opacity: 0.5;
    color: $yellow;
  }
  25% {
    -webkit-text-stroke-width: 0;
    opacity: 1;
    color: rgba(#000, 0.35);
  }
  37.5% {
    -webkit-text-stroke-width: 0;
    opacity: 0.5;
    color: #fff;
  }
  50% {
    -webkit-text-stroke-width: 0;
    opacity: 1;
    color: $yellow;
  }
  62.5% {
    -webkit-text-stroke-width: 0;
    opacity: 0;
    color: #fff;
  }
  75% {
    -webkit-text-stroke-width: 0;
    opacity: 1;
    color: rgba(#000, 0.35);
  }
  87.5% {
    opacity: 0.5;
    color: rgba(#000, 0);
    -webkit-text-stroke-width: 1px;
    -webkit-text-stroke-color: #000;
  }
  100% {
    -webkit-text-stroke-width: 0;
    opacity: 1;
    color: #fff;
  }
}
  • ⑧と同じく、テキストを1文字ずつ分割する際にdata-random 属性に 1~7 のランダムな数字を割り当てるようにします
  • アニメーション自体は作成した text-blink keyframeアニメーションを1回再生しているだけです。
  • animation-delay について、 nth-childでdelayを設定しつつ、 data-random の値によっては順番が多少前後するようにすることで、ある程度のランダム性を確保しています
@for $i from 1 through 14 {
    @for $j from 1 through 7 {
        &:nth-child(#{$i})[data-random='#{$j}'] {
            animation-delay: #{($i - 1) * 0.02s + ($j * 0.02s)};
        }
    }
}

紹介は以上となります!(後半になるにつれ、説明が雑になりすみません)
CSSアニメーションは手っ取り早く始められるのがいいところですし、同じアニメーションでもdurationやイージングによって印象が大きく変わるのが面白いです。
記事のためにサンプルを作成しましたが、やはり動くものを作るのはわくわくしますね。

想像以上に長い記事になってしまいましたが、ここまでお読みくださった方ありがとうございました!

22
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?