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"だけを付け、意味は隣の文字が持ちます。
.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"で足ります。
.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を指します。
.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枚まで増やしました。
.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のままにします。
.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でそれを参照します。
.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="いいね"で名前を持ちます。
.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"だけを付けます。
.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%まで伸ばしました。
.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属性を同時に切り替えます。