こちらの記事は「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という、余分なアニメーションを無効にしたいユーザー向けに用意されているメディアクエリも合わせて使ってみるといいかもしれません。
①下から一文字ずつでてくるやつ
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
をかけた親要素の中で、span
が transform: translateY
で上下しているというシンプルな仕組みです。
下からではなく、上から降ってくる感じにしてもいい感じになります。
②若干角度をつけながら下から一文字ずつ出てくる
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 は複数の値を設定できるため、少し値を追加してみるだけで雰囲気を変えることが出来ます。
③スライドイン
<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-item
に transform: translateX(-100%)
をかけ要素分だけ移動させます。
↓
そして中の span
要素を、親要素と同じ分だけ transform: translateX(100%)
で逆方向に移動させ、元の位置に戻します。
.text-item
に overflow: hidden
をかけることで、.text-item
からはみ出た span
は見えなくなります。
最後に全く同じアニメーションで元の位置に戻すと、スライドインしたようなアニメーションになります。
↓途中経過の様子
clip-path
を使えば span
タグなしでもっとシンプルに同じアニメーションを実現できますが、transform
のほうが動きが軽いので今回はそうしています。
④横からフェードイン&グラデーション
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
<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-wrap
の data-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 & グラデーション
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
のアニメーション後、少し遅れて文字色を赤→白に変化させます。 - 「自分で未来を切り拓け。」みたいなかっこいいキャッチに似合いそうだなと思いながら作りました。
⑦テキストが真ん中から分割された状態で出現
<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で上下に配置します。 -
span
にoverflow: hidden
をかけ、半分しか見えないようにします。 -
span
の疑似要素でテキストを作成し、テキストの上下半分ずつ表示 - あとは
span
の中でtransform: translateY()
でアニメーションさせると真ん中の線からにゅっと出現したかのような演出になります。
⑧ランダムな順番で一文字ずつフェードイン
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};
}
}
- ランダムにすることで、ただのフェードインとはまた違った印象になるかと思います。
⑨デジタルチックな点滅アニメーション
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
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
やイージングによって印象が大きく変わるのが面白いです。
記事のためにサンプルを作成しましたが、やはり動くものを作るのはわくわくしますね。
想像以上に長い記事になってしまいましたが、ここまでお読みくださった方ありがとうございました!