GODRICH

【一行でコピペOK】CSS ダークモード 切り替え アニメーション9選

css ダークモード 切り替え は、ページの配色を light と dark で入れ替えるトグルの動きを CSS だけで作る手法です。この記事では9種類を選び、アイコンの回転から light-dark() 関数まで、実装ごとにコードをそろえました。

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

グリッドの9つは iframe の中で、2秒ごとにライト→ダーク→ライトへ自動で切り替わります。クリックはできず、動きを眺めるだけの一覧です。並び順は、アイコンやノブ単体が変わるもの(01・02・05・06・07)から、パネル全体の配色が一度に切り替わるもの(03・04・08)、最後に3段階の状態を持つトグル(09)という、実装の複雑さが増える順です。項目ごとに要点となるコードだけを載せているので、完全なファイルは zip の中にあります。

01太陽↔月アイコン切替

トグルを押すと太陽アイコンが回転しながら月アイコンへクロスフェードします。実装では300ms・spring系イージングを使うと、勢いよく回りながらも行き過ぎない動きになります。ヘッダーのダークモードボタンや設定メニューに向いています。

400ms회전 180°크로스페이드
.toggle__sun {
  background: $stage-yellow;
  animation: sun $dur-loop $easing infinite;
}
.toggle__moon {
  background: $subject-cream;
  animation: moon $dur-loop $easing infinite;
}

@keyframes sun {
  0%, 15%   { opacity: 1; transform: rotate(0deg); }
  40%, 60%  { opacity: 0; transform: rotate(90deg); }
  85%, 100% { opacity: 1; transform: rotate(0deg); }
}
@keyframes moon {
  0%, 15%   { opacity: 0; transform: rotate(-90deg); }
  40%, 60%  { opacity: 1; transform: rotate(0deg); }
  85%, 100% { opacity: 0; transform: rotate(-90deg); }
}

02ノブスライド

スイッチのトラック上でノブが右へ滑り、背景色も同時に切り替わります。600ms・ease-outで、28px幅のノブがゆっくり移動するぶん状態の変化が目で追いやすくなります。設定画面のトグルやナビ右側のボタンに向いています。

28px 트랙느리게 600ms그림자 추가
.track__bg--dark {
  background: $subject-ink;
  opacity: 0;
  animation: face-dark $dur-loop $easing infinite;
}
.knob {
  transform: translateX(0);
  animation: knob-slide $dur-loop $easing infinite;
}

@keyframes knob-slide {
  0%, 15%   { transform: translateX(0); }
  40%, 60%  { transform: translateX(20px); }
  85%, 100% { transform: translateX(0); }
}

03クリック地点から円形に切替(View Transitions)

トグル位置(パネル右上)を起点に円が広がり、パネル全体がダークモードに覆われます。実際に組み込む際は View Transitions API のクリックイベントに合わせて再生すると、本物のクリック地点から画面全体が切り替わる演出になります。LPヒーローのトグルやアプリの最初の画面に向いています。

중앙에서1.2배속사각 확산
.wipe {
  top: -60px; right: -60px;
  width: 320px; height: 320px;
  border-radius: 50%;
  transform-origin: 78% 22%;
  transform: scale(0);
  animation: wipe-scale $dur-loop $easing infinite;
}

@keyframes wipe-scale {
  0%, 10%   { transform: scale(0); }
  45%, 55%  { transform: scale(1); }
  90%, 100% { transform: scale(0); }
}

04light-dark()一行で切替

2つのスウォッチが交互にフェードし、変数をひとつ切り替えるだけで配色全体が反転する様子を示します。実際の実装では color-scheme を light と dark で切り替え、background や color を light-dark() 関数一発で書けます。対応状況は MDN の light-dark() 対応表 にあり、Chrome 123以降・Safari 17.5以降・Firefox 120以降で使えます。

변수 2개전환 300ms테두리 포함
.chip__swatch--light {
  background: $color;
  animation: face-light $dur-loop $easing infinite;
}
.chip__swatch--dark {
  background: $stage-yellow;
  opacity: 0;
  animation: face-dark $dur-loop $easing infinite;
}

@keyframes face-light { 0%, 15% { opacity: 1; } 40%, 60% { opacity: 0; } 85%, 100% { opacity: 1; } }
@keyframes face-dark  { 0%, 15% { opacity: 0; } 40%, 60% { opacity: 1; } 85%, 100% { opacity: 0; } }

05日食マスク切替

月形のマスクが横から滑ってきて太陽アイコンを覆い、ダークモードへ切り替わります。600ms・spring系イージングで同じキーフレームを往復させるだけなので、戻すときに新しいアニメーションを足す必要がありません。ブランドを見せたいトグルやプロダクト紹介ページに向いています。

500ms그림자 진하게역방향 재생
.mask {
  bottom: clamp(6px, 4vw, 12px);
  left: clamp(0px, 2vw, 4px);
  width: 26px; height: 26px;
  border-radius: 50%;
  background: $stage-ink;
  transform: translateX(-60px);
  animation: mask-slide $dur-loop $easing infinite;
}

@keyframes mask-slide {
  0%, 15%   { transform: translateX(-60px); }
  40%, 60%  { transform: translateX(4px); }
  85%, 100% { transform: translateX(-60px); }
}

06カード反転切替

トグルカードがY軸で反転し、表の太陽から裏の月に変わります。perspective: 900pxとbackface-visibility: hiddenを両方指定しないと、裏面が透けて一瞬だけ両面が重なって見えます。カード型の設定スイッチやプロフィールメニューに向いています。

600msperspective 800px그림자 따라 이동
.card__inner {
  transform-style: preserve-3d;
  animation: flip $dur-loop $easing infinite;
}
.card__face {
  backface-visibility: hidden;
}
.card__face--back {
  transform: rotateY(180deg);
}

@keyframes flip {
  0%, 15%   { transform: rotateY(0deg); }
  50%       { transform: rotateY(180deg); }
  85%, 100% { transform: rotateY(360deg); }
}

07アイコンモーフィング

太陽アイコンの光線がrotateとscaleY(0)で折りたたまれ、代わりに月のクレーターが浮かび上がります。220ms・pop系イージングで素早く畳まれるので、アイコンボタンやツールバーのトグルでも動きが邪魔になりません。SVGを使わず、divとborder-radiusだけで作っています。

SVG path350mseasing 스냅
.icon__ray {
  transform-origin: 50% 17px;
  transform: rotate(calc(var(--i) * 60deg)) translateY(-17px) scaleY(1);
  animation: ray-fold $dur-loop $easing infinite;
}

@keyframes ray-fold {
  0%, 15%   { opacity: 1; transform: rotate(calc(var(--i) * 60deg)) translateY(-17px) scaleY(1); }
  40%, 60%  { opacity: 0; transform: rotate(calc(var(--i) * 60deg)) translateY(-17px) scaleY(0); }
  85%, 100% { opacity: 1; transform: rotate(calc(var(--i) * 60deg)) translateY(-17px) scaleY(1); }
}

08全体の色がフェード切替

背景と文字色が同時にフェードし、ボタンなしでパネル全体の配色が入れ替わります。600ms・ease-outでopacityだけを動かすので、フィルターやぼかしを足さなくても十分滑らかです。ページ全体のテーマ切替やドキュメントビューアに向いています。

800ms필터 blur 2px즉시 전환
.panel__face--light {
  opacity: 1;
  animation: face-light $dur-loop $easing infinite;
}
.panel__face--dark {
  opacity: 0;
  animation: face-dark $dur-loop $easing infinite;
}

@keyframes face-light { 0%, 15% { opacity: 1; } 40%, 60% { opacity: 0; } 85%, 100% { opacity: 1; } }
@keyframes face-dark  { 0%, 15% { opacity: 0; } 40%, 60% { opacity: 1; } 85%, 100% { opacity: 0; } }

09ライト・ダーク・自動の3段階

3コマのトラック上でインジケーターがライト→ダーク→自動の順に移動します。300ms・spring系イージングでbox-shadow付きのつまみが弾むように止まるので、選んだ状態がひと目で分かります。詳細設定ページやデスクトップアプリの環境設定に向いています。

3단 트랙350ms인디케이터 그림자
.segmented__thumb {
  transform: translateX(0);
  animation: thumb-move $dur-loop $easing infinite;
}

@keyframes thumb-move {
  0%, 22%   { transform: translateX(0); }
  33%, 55%  { transform: translateX(32px); }
  66%, 88%  { transform: translateX(64px); }
  100%      { transform: translateX(0); }
}

どこで崩れるか — 落とし穴

9つとも opacity と transform しか動かさない理由は、background-color や width を直接アニメーションさせるとブラウザがレイアウトや再描画を毎フレーム計算し直すからです。MDN の prefers-color-scheme にあるとおり、OS側のダークモード設定はあくまで初期値の目安で、ここで作るトグルのように後から手動で上書きできる状態と合わせて設計する必要があります。

実際に9つを並べてみて気づいたのが、.maskや.knob、.iconのようなクラス名がどれも短く汎用的なことです。既存のコンポーネントに同じ名前のクラスがあると、意図しないスタイルが重なって片方だけ動かなくなります。zipのファイルではそのままの名前で置いていますが、実際のプロジェクトに移すときはコンポーネント単位で接頭辞を付けるか、CSS Modulesのようなスコープの仕組みに通すことをおすすめします。

もうひとつの罠は、この9ファイルがどれもデモ用の無限ループだけで、クリックでテーマを切り替える JavaScript を持たないことです。修正済みの9ファイルを開くには kjdwbr3q という文字列が必要です。下のカードから zip を取ってください。

アクセシビリティ

9つとも prefers-reduced-motion: reduce で animation: none になり、既定ではライト状態のまま止まります。ただし03の円形展開と06のカード反転は「動きを止める」と「状態を伝える」が別物なので、止めた後もトグルの見た目でどちらのテーマかが分かるようにしてあります。09の3段階トグルだけは、動きを止めても選択中のコマにインジケーターをそのまま表示します。

@media (prefers-reduced-motion: reduce) {
  .toggle__sun, .toggle__moon, .knob, .wipe,
  .card__inner, .icon__ray, .segmented__thumb { animation: none !important; }
}

ここで扱わなかった CSS アニメーションは CSS カテゴリ に一覧があり、サイトの成り立ちは 紹介ページ に書いています。

FAQ

9つとも本番のトグルボタンにそのまま使えますか?

デモのCSSはどれも$dur-loopの無限ループで自動再生する見た目確認用のファイルです。実際にクリックでテーマを切り替えるには、ボタンのクリックイベントで<html>要素にdata-theme属性を付け外しし、その属性の有無で各アニメーションのクラスを切り替えるJavaScriptを別途書く必要があります。zipのreact/フォルダには、そのクラス切り替えまで含めたコンポーネント版が入っています。

CSSだけでライト・ダークを判定できますか?

OS側の設定に合わせるだけならprefers-color-schemeメディアクエリで判定できますが、ここで紹介した9つのようにユーザーがボタンで手動切り替えできるようにするには、選んだ状態をdata属性かclassとして保持する必要があります。04のlight-dark()関数も、あくまでcolor-schemeプロパティで指定した値を参照するだけなので、その値を切り替えるには別途JavaScriptか設定UIが必要です。

スマホでも同じアニメーションが見えますか?

はい、動きます。09の3段階トグルのようにタップ操作の当たり判定が狭いUIは、96px幅のトラックのままだとスマホの指では押しづらいので、実機では最小44px以上のタップ領域を別途確保することをおすすめします。

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

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