SVGアイコンは、画像ファイルとして表示するだけでなく、CSSから色、回転、拡大縮小、線の描画などをアニメーションできます。ローディング、メニューボタン、矢印、チェックマークなど、UIの細かな反応をJavaScriptなしで作れるのが魅力です。
この記事では、インラインSVGをCSSで操作する基本から、ホバー、線描画、パーツごとの遅延、モーション軽減設定まで、コピペして応用できる実装を紹介します。
CSSでSVGを動かすための前提
SVG内部のpathやcircleをCSSで選択したい場合は、SVGをHTMLへインラインで記述します。<img src="icon.svg">として読み込んだ外部SVGの内部要素は、通常のページCSSから直接選択できません。
<button class="icon-button" type="button" aria-label="更新">
<svg class="icon icon-refresh" viewBox="0 0 24 24" aria-hidden="true">
<path d="M20 11a8 8 0 1 0-2.34 5.66" />
<path d="M20 4v7h-7" />
</svg>
</button>
.icon {
width: 24px;
height: 24px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
currentColorを使うと、アイコンは親要素のcolorを引き継ぎます。通常、ホバー、無効状態などの色管理が簡単になります。
実装例1:ホバーで矢印を動かす
<a class="more-link" href="#">
詳しく見る
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
</a>
.more-link svg {
width: 20px;
transition: transform .2s ease;
}
.more-link:hover svg,
.more-link:focus-visible svg {
transform: translateX(5px);
}
アイコン単体ではなく、リンク全体のホバーとキーボードフォーカスをトリガーにします。
実装例2:更新アイコンを回転させる
.icon-refresh {
transform-box: fill-box;
transform-origin: center;
}
.icon-button:hover .icon-refresh,
.icon-button:focus-visible .icon-refresh {
animation: icon-spin .7s ease-in-out;
}
@keyframes icon-spin {
to {
transform: rotate(360deg);
}
}
SVGの変形基準を安定させるため、transform-box: fill-boxとtransform-origin: centerをセットで指定します。
実装例3:ハートを鼓動させる
.favorite-button svg {
transform-box: fill-box;
transform-origin: center;
}
.favorite-button:hover svg,
.favorite-button:focus-visible svg {
animation: heart-beat .45s ease-in-out;
}
@keyframes heart-beat {
0%, 100% { transform: scale(1); }
45% { transform: scale(1.22); }
70% { transform: scale(.96); }
}
拡大率を大きくしすぎると周囲のUIへ接触します。アイコンボタンの領域は固定し、SVGだけを変形させます。
実装例4:チェックマークを線描画する
stroke-dasharrayで線を破線化し、stroke-dashoffsetを線の長さから0へ変化させると、線が描かれるように見せられます。
<svg class="check-icon" viewBox="0 0 48 48" aria-hidden="true">
<circle cx="24" cy="24" r="20" />
<path class="check-line" pathLength="1" d="M14 24l7 7 14-16" />
</svg>
.check-line {
stroke-dasharray: 1;
stroke-dashoffset: 1;
}
.is-complete .check-line {
animation: draw-check .5s ease forwards;
}
@keyframes draw-check {
to {
stroke-dashoffset: 0;
}
}
pathLength="1"を指定すると、線の実寸を計測せず、1を基準にdash値を扱えます。クラスの付与はサーバー出力、チェックボックス状態、既存UIの状態クラスなどに合わせてください。
実装例5:円形ローダーを作る
<svg class="loader" viewBox="0 0 50 50" role="img" aria-label="読み込み中">
<circle class="loader-track" cx="25" cy="25" r="20" />
<circle class="loader-progress" cx="25" cy="25" r="20" />
</svg>
.loader {
width: 40px;
animation: loader-rotate 1s linear infinite;
}
.loader circle {
fill: none;
stroke-width: 5;
}
.loader-track {
stroke: #e6e6e6;
}
.loader-progress {
stroke: #ff2f63;
stroke-linecap: round;
stroke-dasharray: 90 150;
stroke-dashoffset: 0;
}
@keyframes loader-rotate {
to { transform: rotate(360deg); }
}
実装例6:メニューアイコンを閉じるアイコンへ変形する
<button class="menu-button" type="button" aria-expanded="false">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path class="line line-top" d="M4 7h16" />
<path class="line line-middle" d="M4 12h16" />
<path class="line line-bottom" d="M4 17h16" />
</svg>
<span class="screen-reader-text">メニュー</span>
</button>
.menu-button .line {
transform-box: fill-box;
transform-origin: center;
transition: transform .25s ease, opacity .2s ease;
}
.menu-button[aria-expanded="true"] .line-top {
transform: translateY(5px) rotate(45deg);
}
.menu-button[aria-expanded="true"] .line-middle {
opacity: 0;
transform: scaleX(0);
}
.menu-button[aria-expanded="true"] .line-bottom {
transform: translateY(-5px) rotate(-45deg);
}
開閉状態はaria-expandedと同期させます。アニメーション自体はCSSだけですが、実際のナビ開閉にはボタン状態を切り替える処理が必要です。
実装例7:ベルを揺らす
.notification-button svg {
transform-box: fill-box;
transform-origin: 50% 10%;
}
.notification-button:hover svg,
.notification-button:focus-visible svg {
animation: bell-ring .6s ease-in-out;
}
@keyframes bell-ring {
0%, 100% { transform: rotate(0); }
25% { transform: rotate(12deg); }
50% { transform: rotate(-10deg); }
75% { transform: rotate(6deg); }
}
実装例8:複数パーツを順番に表示する
.spark-icon path {
opacity: 0;
transform: scale(.6);
transform-box: fill-box;
transform-origin: center;
}
.spark-button:hover .spark-icon path,
.spark-button:focus-visible .spark-icon path {
animation: spark-in .35s ease forwards;
}
.spark-icon path:nth-child(2) { animation-delay: .08s; }
.spark-icon path:nth-child(3) { animation-delay: .16s; }
@keyframes spark-in {
to {
opacity: 1;
transform: scale(1);
}
}
パーツ数が多いとCSSが複雑になります。2〜4個程度の短い演出に留めると管理しやすく、UIも落ち着きます。
クリック中・選択状態をCSSで表現する
ホバーだけでなく、:active、:checked、aria-pressed="true"などの状態を利用できます。
.like-button[aria-pressed="true"] svg {
fill: currentColor;
animation: heart-beat .45s ease-in-out;
}
.icon-toggle:checked + label svg {
transform: rotate(180deg);
}
prefers-reduced-motionに対応する
OSで動きを減らす設定を有効にしている利用者へ、不要な回転や移動を強制しないようにします。
@media (prefers-reduced-motion: reduce) {
.icon,
.icon *,
.loader {
animation-duration: .01ms !important;
animation-iteration-count: 1 !important;
transition-duration: .01ms !important;
}
}
状態変化そのものは、色、塗り、下線、テキストなど動き以外でも分かるようにしてください。
よくある失敗
- imgで読み込んだSVG内部を選択しようとする:内部パーツを動かすならインラインSVGにします。
- transform-originを指定しない:回転や拡大の中心が意図しない位置になります。
- hoverだけに依存する:キーボード操作の
:focus-visibleや状態属性も用意します。 - 無限アニメーションを多用する:視線を奪い、消費電力や読みやすさにも影響します。
- SVGへ固定色を直接書く:
currentColorを利用すると状態別の色変更が容易です。 - 意味のあるアイコンをaria-hiddenにする:テキストがない場合はボタンへ適切なラベルを付けます。
- モーション軽減設定を無視する:
prefers-reduced-motionで演出を抑えます。
パフォーマンスを保つコツ
transformとopacityを中心に使う- 一度に動かすパーツを少なくする
- 常時ループより、操作時の短いフィードバックを優先する
- SVGの不要なパスやメタデータを削減する
- アイコンの表示領域を固定してレイアウトを動かさない
まとめ
SVGアイコンはインラインで配置すると、CSSだけで各パーツを細かく制御できます。単純な移動や回転はtransform、線描画はstroke-dasharrayとstroke-dashoffset、状態管理はホバー、フォーカス、属性セレクターを使うのが基本です。
アニメーションは目立たせるためではなく、押せること、処理中であること、状態が変わったことを伝えるために使うと効果的です。短く、意味があり、動きを減らす設定にも対応した演出を心がけましょう。

