GODRICH

CSS アイコン アニメーション 9選 — 一行でコピペOK

css アイコン アニメーションとは、再生ボタンや通知ベル、更新マークのような定番のインターフェースアイコンを、文字ではなく座標を持つSVGのpathとして描き、状態が変わるたびに動かす手法を指します。書体に頼らないぶん、フォントが変わってもアイコンの形は崩れません。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

並びはアイコンが画面で担う役割の順に揃えています。バッジやロゴのように置いておくだけで視線を引く装飾(01・02)から始まり、押すたびに状態が変わるアイコンボタン(03〜07)を経て、文字の隣にそっと添えるだけの装飾(08・09)で終わります。9つとも動かしているのはtransformとopacityだけですが、線を描く3つ(01・02・09)だけはstroke-dasharrayとdashoffsetも使います。この2つはtopやwidthと違ってレイアウトを再計算しないペイント段階のプロパティで、MDNのstroke-dasharrayにも説明があります。

01ストローク描画

星形の輪郭線(pathLength="100")のstroke-dashoffsetを100から0へ動かして手描きのように浮かび上がらせ、少し止まってから逆方向の-100まで動かして消します。JavaScriptなしで自動的に繰り返すだけの、新着・更新バッジの横に添える装飾アイコンなので、svgにはaria-hidden="true"だけを付け、意味は隣の文字が持ちます。

stroke-dasharray 100600ms빠르게 300ms
.badge__star {
  fill: none; stroke: $color; stroke-width: 1.6;
  stroke-dasharray: 100;                 // pathLength="100" なので % として扱える
  animation: draw $duration $easing infinite;
}
@keyframes draw {
  0%   { stroke-dashoffset: 100; }
  55%  { stroke-dashoffset: 0; }
  80%  { stroke-dashoffset: 0; }
  100% { stroke-dashoffset: -100; }
}

02ロゴのリビール

ひし形のロゴマークを線版と塗り版の2枚のpathとして重ね、線版のstroke-dashoffsetが100から0まで描き切ったところで塗り版がopacityで浮かび上がり、また線だけに戻って繰り返します。LPのヒーローでブランドロゴを登場させる想定で、ワードマーク「GODRICH」は隣に普通の文字として置くので、svg側はaria-hidden="true"で足ります。

stroke → fill 2단계600ms라인만 남기기
.mark__fill {
  fill: $color; opacity: 0;
  animation: fill-in $duration $easing infinite;
}
.mark__line {
  fill: none; stroke: $color; stroke-width: 1.6;
  stroke-dasharray: 100;
  animation: line-draw $duration $easing infinite;
}
@keyframes line-draw {
  0%   { stroke-dashoffset: 100; opacity: 1; }
  55%  { stroke-dashoffset: 0;   opacity: 1; }
  85%  { opacity: 0; }
  100% { stroke-dashoffset: 0;   opacity: 0; }
}
@keyframes fill-in {
  0%, 55%  { opacity: 0; }
  75%, 85% { opacity: 1; }
}

03アイコンモーフィング

再生の三角形と一時停止の2本の棒を別々の<svg>として同じグリッド位置(grid-area)に重ね、クリックのたびにopacityとscale(.85→1)で入れ替える、動画・音声プレーヤーの再生ボタンです。アイコンだけのボタンなので、状態が変わるとsvg内の<title>を「再生」「一時停止」に書き換え、buttonはaria-labelledbyでそのtitleを指します。

opacity 크로스페이드220msscale .85→1
.ic {
  grid-area: 1 / 1; fill: currentColor;
  transition: opacity $duration $easing, transform $duration $easing;
  transform-box: fill-box; transform-origin: 50% 50%;
}
.ic--play  { opacity: 1; }
.ic--pause { opacity: 0; transform: scale(.85); }
.iconbtn[aria-pressed="true"] .ic--play  { opacity: 0; transform: scale(.85); }
.iconbtn[aria-pressed="true"] .ic--pause { opacity: 1; transform: scale(1); }

04ベルが揺れる

鐘を包む<g>transform-box: fill-boxで上端(50% 0%)を軸に左右へ揺らし、右上のバッジの丸も同じタイミングでscaleパルスする、自動ループの通知ボタンです。最初は揺れる区間をループ全体の15%に収めたところ、測定で動いたフレームが4枚しかなく基準の6枚に届かなかったので、30%まで広げて7枚まで増やしました。

transform-box: fill-box진폭 8deg배지 pulse
.bell { transform-box: fill-box; transform-origin: 50% 0%; }
.badge-dot { fill: $color; transform-box: fill-box; transform-origin: 50% 50%; }

.iconbtn.is-demo .bell { animation: shake $dur-loop $easing infinite; }
@keyframes shake {
  0%, 30%, 100% { transform: rotate(0); }
  4%  { transform: rotate(8deg); }
  8%  { transform: rotate(-8deg); }
  16% { transform: rotate(-6deg); }
  24% { transform: rotate(-3deg); }
}

05リフレッシュ回転

2本の矢印から成る更新アイコンは24×24のビューボックスの真ん中に収まっていないため、<g>transform-box: fill-boxを与えて図形自身の境界ボックスを軸にし、クリックのたびに600msで一回転させます。リストやフィードを読み込み直すボタンの想定で、aria-label="更新"をbuttonに付け、svg自体はaria-hiddenのままにします。

transform-box: fill-box360deg 600ms연속 회전(.is-demo)
.refresh {
  transform-box: fill-box;
  transform-origin: 50% 50%;
}
.iconbtn.is-spinning .refresh { animation: spin-once $duration $easing; }
@keyframes spin-once { to { transform: rotate(360deg); } }

06メニューアイコンモーフ

点が3つ並んだメニューのpathとXのpathを同じグリッド位置に重ね、回転はいっさい使わずopacityだけで220msかけて入れ替えます。線が重なって回転しながらXになる一般的な作り方とは別で、開閉のたびにsvg内<title>を「メニューを開く」「メニューを閉じる」に更新し、buttonはaria-labelledbyでそれを参照します。

점 3개 → Xopacity 크로스페이드220ms
.ic { grid-area: 1 / 1; fill: currentColor; transition: opacity $duration $easing; }
.ic--dots { opacity: 1; }
.ic--x    { opacity: 0; }
.iconbtn[aria-expanded="true"] .ic--dots { opacity: 0; }
.iconbtn[aria-expanded="true"] .ic--x    { opacity: 1; }

.iconbtn.is-demo .ic--dots { animation: dots-loop $dur-loop $easing infinite; }
.iconbtn.is-demo .ic--x    { animation: x-loop $dur-loop $easing infinite; }

07ハートパルス

輪郭だけのハートと塗りつぶしたハートを重ねておき、押すと塗り側がopacityで現れると同時に両方のscaleが1から1.3まで跳ねてから1に戻ります。投稿や商品のいいねボタンを想定し、buttonはaria-pressedを切り替えつつaria-label="いいね"で名前を持ちます。

scale 1→1.3→1220msfill 크로스페이드
.ic--fill { fill: $color; opacity: 0; }
.iconbtn[aria-pressed="true"] .ic--fill {
  opacity: 1;
  transform: scale(1);
}
@keyframes fill-loop {
  30%      { transform: scale(1.3); }
  40%, 60% { opacity: 1; }
}

08矢印移動

下向きの矢印ひとつが2秒周期でtranslateYを0から8pxまで往復し、ページの続きを示すスクロール誘導として自動ループします。隣にある「もっと見る」という見える文字が意味を持つ装飾なので、svgにはaria-hidden="true"だけを付けます。

translateY 8px2s 루프ease-spring
.cue__arrow { color: $color; animation: bob $duration $easing infinite; }

@keyframes bob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(8px); }   // $sp-2
}

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

09チェック描画

polylineで作ったチェックのstroke-dashoffsetを100から0まで動かして左から描き切り、少し止まってから-100まで動かして消す自動ループの確認アイコンです。最初はアイコンを26pxで作ったところ測定した変化面積が基準の0.08%を下回る0.04%だったため、64pxに広げ線の太さも3.2pxまで太くして0.31%まで伸ばしました。

stroke-dashoffsetpolyline 체크300ms
.confirm__check {
  stroke-dasharray: 100;
  animation: draw $duration $easing infinite;
}
@keyframes draw {
  0%   { stroke-dashoffset: 100; }
  55%  { stroke-dashoffset: 0; }
  80%  { stroke-dashoffset: 0; }
  100% { stroke-dashoffset: -100; }
}

回転の軸はどこで狂うのか — 落とし穴

一番よく踏む落とし穴は回転の軸です。<g><path>をそのままrotate()させると、軸はその要素が属するSVGビューポートの(0,0)〜(24,24)を基準に決まってしまい、04のベルや05のリフレッシュのように図形がビューボックスの片側に寄っていると、軸ごと変な場所へ飛びます。transform-box: fill-boxを足すと、図形自身の境界ボックスが軸になります。この値はChrome 64・Edge 79・Firefox 55、iOSを含むSafari 11以降で使え、caniuseの対応表を見てもまず引っかかりません。04と09は測定値が基準に届かず一度ずつ直しており、そのとき直した値のまま入ったzipの解凍パスワードがdatzbfgjで、下のカードから受け取れます。

もうひとつはグリフの選び方です。チェックや矢印をのような文字として置くと、閲覧する端末のフォントによって表示されなかったり、まったく違う形に化けたりします。9つすべてをSVGのpathで描いたのはこのためで、文字コードに運命を預けません。

アクセシビリティ

アイコンだけのボタン5つ(03・04・05・06・07)は名前がないとスクリーンリーダーに「ボタン」としか読まれず、何をする操作か伝わりません。03・06はsvg内の<title>とbuttonのaria-labelledbyで名前を持たせ、状態が変わるたびにそのtitleを書き換えます。04・05・07は状態によらずaria-labelで固定の名前を持たせます。

<button aria-labelledby="t3">
  <svg><title id="t3">再生</title><path d="M8 5l11 7-11 7z"/></svg>
</button>

残る4つ(01・02・08・09)は隣に見える文字がすでに意味を伝える装飾なので、svgにはaria-hidden="true"だけを付けます。

<svg aria-hidden="true"><path class="badge__star" d="M12 2l2.9 6.9L22 9.3z"/></svg> 新着

prefers-reduced-motion: reduceでは9つとも動きを止め、完成した形(描き終えたチェック、塗りきったハート)だけを残します。動きだけを止めて結果まで消すと「保存できたか」のような情報そのものが失われるからです。アイコン以外のCSSアニメーションはこちらのカテゴリーに、この記事を書いた背景はサイトについてにまとめています。

FAQ

アイコンをテキストの絵文字(✓や→)で代用してはいけないのですか?

代用はできますが、避けたほうがいいです。絵文字や記号はOSやブラウザに載っているフォントに描画を頼るため、端末によって太さや形が変わったり、対応するグリフ自体がなく四角い枠だけが表示されたりします。SVGのpathならファイルの中に形そのものを持つので、どの端末を開いても同じ輪郭になります。

transform-box: fill-boxを外すとどうなりますか?

rotate()の軸がSVGビューポートの左上を基準に戻ってしまいます。05のリフレッシュで試すと、矢印の図形がビューボックスの隅に寄ったまま回るので、軸が中心からずれてぐらついて見える回り方になります。

アイコンボタン5つはReactでどう状態を持たせていますか?

zipのreact/フォルダの各コンポーネントは、playing・open・likedのような状態をひとつずつuseStateで持ち、クラスの付け外しではなくclassNameの分岐でopacityとaria属性を同時に切り替えます。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。