SVGアイコンは、CSSだけで移動、回転、拡大縮小、線の描画、パーツの変形を加えられます。ボタンの反応、処理中の表示、選択状態のフィードバックなど、短く意味のあるUIアニメーションに向いています。
この記事では、そのままコピペできる矢印、更新、ハート、メニュー、チェック、ローダーの実装をまとめます。記事内にはCodePenを後から埋め込める専用枠と、デモ用のHTML・CSS・JavaScriptも用意しています。
SVGをCSSで動かす前提
SVG内部のpathをページのCSSから選択するには、SVGをHTMLへインラインで記述します。<img src="icon.svg">で読み込んだSVGの内部要素は、通常のページCSSから直接操作できません。
<button class="icon-button" type="button" aria-label="更新">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M20 11a8 8 0 1 0-2.34 5.66"></path>
<path d="M20 4v7h-7"></path>
</svg>
</button>
.icon-button svg {
width: 24px;
height: 24px;
fill: none;
stroke: currentColor;
stroke-width: 2;
}
currentColorを使うと、SVGの色が親要素のcolorへ追従します。通常、ホバー、選択、無効状態の色管理が簡単になります。
実践例1:矢印を横へ動かす
.more-link svg {
transition: transform .2s ease;
}
.more-link:hover svg,
.more-link:focus-visible svg {
transform: translateX(6px);
}
実践例2:更新アイコンを回転させる
.refresh-icon {
transform-box: fill-box;
transform-origin: center;
}
.refresh-button:hover .refresh-icon {
animation: spin .7s ease-in-out;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
SVGは変形の基準が分かりにくくなることがあるため、transform-box: fill-boxとtransform-origin: centerをセットで指定すると安定します。
実践例3:ハートを鼓動させる
.favorite[aria-pressed="true"] svg {
fill: currentColor;
animation: heart-beat .45s ease-in-out;
}
@keyframes heart-beat {
0%, 100% { transform: scale(1); }
45% { transform: scale(1.24); }
72% { transform: scale(.96); }
}
実践例4:チェックマークを描画する
<svg viewBox="0 0 48 48" aria-hidden="true">
<path class="check-line" pathLength="1" d="M14 24l7 7 14-16"></path>
</svg>
.check-line {
stroke-dasharray: 1;
stroke-dashoffset: 1;
}
.is-complete .check-line {
animation: draw .5s ease forwards;
}
@keyframes draw {
to { stroke-dashoffset: 0; }
}
pathLength="1"を使うと、実際の線の長さを測らず、1を基準にdash値を扱えます。
実践例5:円形ローダー
.loader {
width: 42px;
animation: spin 1s linear infinite;
}
.loader circle {
fill: none;
stroke: #ff2f63;
stroke-width: 5;
stroke-linecap: round;
stroke-dasharray: 90 150;
}
実践例6:メニューを閉じるアイコンへ変形する
.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);
}
CodePen埋め込み用エリア
CodePenでPenを作成したら、上の枠をEmbedコードへ差し替えてください。以下をHTML、CSS、JSの各パネルへ貼り付けると、4種類のアイコンをまとめて確認できます。
HTML
<main class="svg-demo">
<button class="icon-button icon-button--arrow" type="button">
<span>進む</span>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M5 12h14M13 6l6 6-6 6"></path>
</svg>
</button>
<button class="icon-button icon-button--refresh" type="button" aria-label="更新">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M20 11a8 8 0 1 0-2.34 5.66"></path>
<path d="M20 4v7h-7"></path>
</svg>
</button>
<button class="icon-button icon-button--favorite" type="button" aria-pressed="false">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.6l-1-1a5.5 5.5 0 0 0-7.8 7.8l1 1L12 21l7.8-7.6 1-1a5.5 5.5 0 0 0 0-7.8Z"></path>
</svg>
<span>LIKE</span>
</button>
<button class="menu-button" type="button" aria-expanded="false" aria-label="メニューを開く">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path class="line line-top" d="M4 7h16"></path>
<path class="line line-middle" d="M4 12h16"></path>
<path class="line line-bottom" d="M4 17h16"></path>
</svg>
</button>
</main>
CSS
* {
box-sizing: border-box;
}
body {
min-height: 100vh;
margin: 0;
display: grid;
place-items: center;
padding: 24px;
background: #f2f2f2;
color: #111;
font-family: system-ui, sans-serif;
}
.svg-demo {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 16px;
}
.icon-button,
.menu-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
min-width: 64px;
min-height: 56px;
padding: 12px 18px;
border: 2px solid #111;
border-radius: 14px;
background: #fff;
color: #111;
font-weight: 800;
cursor: pointer;
}
svg {
width: 26px;
height: 26px;
overflow: visible;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.icon-button--arrow svg {
transition: transform .22s ease;
}
.icon-button--arrow:hover svg,
.icon-button--arrow:focus-visible svg {
transform: translateX(6px);
}
.icon-button--refresh svg {
transform-box: fill-box;
transform-origin: center;
}
.icon-button--refresh:hover svg,
.icon-button--refresh:focus-visible svg {
animation: icon-spin .7s ease-in-out;
}
.icon-button--favorite svg {
transform-box: fill-box;
transform-origin: center;
transition: fill .2s ease;
}
.icon-button--favorite[aria-pressed="true"] {
border-color: #ff2f63;
color: #ff2f63;
}
.icon-button--favorite[aria-pressed="true"] svg {
fill: currentColor;
animation: heart-beat .45s ease-in-out;
}
.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);
}
@keyframes icon-spin {
to { transform: rotate(360deg); }
}
@keyframes heart-beat {
0%, 100% { transform: scale(1); }
45% { transform: scale(1.24); }
72% { transform: scale(.96); }
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: .01ms !important;
animation-iteration-count: 1 !important;
transition-duration: .01ms !important;
}
}
JS
const favorite = document.querySelector(".icon-button--favorite");
const menu = document.querySelector(".menu-button");
favorite.addEventListener("click", () => {
const pressed = favorite.getAttribute("aria-pressed") === "true";
favorite.setAttribute("aria-pressed", String(!pressed));
});
menu.addEventListener("click", () => {
const expanded = menu.getAttribute("aria-expanded") === "true";
menu.setAttribute("aria-expanded", String(!expanded));
menu.setAttribute("aria-label", expanded ? "メニューを開く" : "メニューを閉じる");
});
よくある失敗
- 外部SVGの内部を選択する:パーツを動かす場合はインラインSVGを使います。
- transform-originを指定しない:回転や拡大の中心が意図しない位置になります。
- hoverだけに依存する:
:focus-visibleや状態属性にも対応します。 - ボタンの状態とaria属性がずれる:
aria-pressedやaria-expandedを見た目と同期します。 - 無限アニメーションを多用する:操作時の短いフィードバックを優先します。
- 動きを減らす設定を無視する:
prefers-reduced-motionで演出を抑えます。
まとめ
SVGアイコンをCSSで動かす基本は、インラインSVG、currentColor、transform、状態セレクターの組み合わせです。線描画にはstroke-dasharrayとstroke-dashoffsetを使います。
アニメーションは装飾のためだけでなく、押せること、処理中であること、状態が変わったことを伝えるために使うと効果的です。短く、読みやすく、キーボード操作とモーション軽減設定にも対応させましょう。
