HOME / BLOG
BLOG

SVGアイコンをCSSだけでアニメーションさせる方法

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-boxtransform-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埋め込み用エリア

SVGアイコンをCSSだけでアニメーションさせるデモ
ここにCodePenのEmbedコードを貼り付けます。Pen作成後、このブロックと埋め込みタグを差し替えてください。

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-pressedaria-expandedを見た目と同期します。
  • 無限アニメーションを多用する:操作時の短いフィードバックを優先します。
  • 動きを減らす設定を無視する:prefers-reduced-motionで演出を抑えます。

まとめ

SVGアイコンをCSSで動かす基本は、インラインSVG、currentColortransform、状態セレクターの組み合わせです。線描画にはstroke-dasharraystroke-dashoffsetを使います。

アニメーションは装飾のためだけでなく、押せること、処理中であること、状態が変わったことを伝えるために使うと効果的です。短く、読みやすく、キーボード操作とモーション軽減設定にも対応させましょう。

参考資料