BLOG

CSS変数 × ScrollTriggerで作る、ダークモード対応背景色アニメーション

INDEX

説明

今回は過去の記事の、「CSS変数とlight-dark()関数を組み合わせて、シンプルかつ効率的にダークモード対応しよう」の応用で、スクロールに合わせて背景色を変更するアニメーションを、light-dark()関数と組み合わせて実装していきます。

ScrollTriggerとは?

ScrollTriggerは、アニメーションライブラリ「GSAP」のプラグインの一つで、スクロール位置に応じてアニメーションを発火・制御することができます。
視差効果やセクションごとのフェードイン、ピン固定など、インタラクティブなスクロール演出を非常に簡単に実装できます。
GSAPといえば、ScrollTriggerは以前から無料プランで使用できましたが、2025/4/29以降はすべてのプラグインが無料プランの対象となったため、興味がある方はぜひ使ってみてください。

完成版

今回のサンプルでは、縦長の1ページのコーポレートサイトを想定しています。
ウィンドウ下部にトグルボタンを追従させ、任意でライトモード/ダークモードの切り替えができるよう実装しました。
また、コーポレートサイトということで、採用を目立たせるためにGSAPのScrollTriggerで該当セクションに入ったらテーマが切り替わるアニメーションや、ユーザー再訪時に、前回見ていたテーマで閲覧ができるように、localStrageでブラウザにテーマを保存する機能も取り入れています。

実装手順

手順1(HTML / ページの枠組作成)

今回はアニメーションの実装がメインのため、サイト全体の設計の説明は割愛します。
HTMLでは、htmlタグに対して、あらかじめdata-theme属性を付与しておきます。

<html data-theme="light">
 <body class="dark-scroll">
 <!-- 追従トグルボタン -->
 <div class="p-toggle">
      <p class="p-toggle__text p-toggle__text--light is-active">Light</p>
      <button id="toggleButton" type="button" class="p-toggle__button" aria-label="モード切替"></button>
      <p class="p-toggle__text p-toggle__text--dark">Dark</p>
  </div>

  <!-- コンテンツ記載 -->
  .....
 </body>
</html>

手順2(CSS / スタイル調整・カラーの変数宣言)

CSSのルート要素(:root)に対し、デフォルトのテーマカラーと、ダークモード用のテーマカラーの変数を定義しておきます。
変数でまとめておくことで、コンテンツ全体の色を変更する際も一括で実行することができます。

デザインによって、同じ色でも微妙な濃淡の違いがあるため、背景色で使うカラーには、--bg-color、文字色で使う場合には--text-colorなどを指定しています。
ダークモードでもライトモードでも色が変わらないパーツがあるかと思いますが、私の場合は混在しないように、 --unique-〇〇:と命名しています。(いずれもプロジェクトや個人で管理しやすいような命名が好ましいです。)
モード変更で切り替わる色については、color-schemeプロパティにライトモード用とダークモード用の色の値を設定し管理します。

"採用セクション"が画面内に入った時、カラーを逆転させるために、各data-theme属性の変数に対してクラス名.is-reversedを付与した変数も用意します。後ほど記述するJavaScriptでクラス名.is-reversedを付け外しすることにより、設定中のテーマとは逆のカラーに切り替えることができます。
※ページのdata-theme属性そのものを変更する方法もありますが、"採用セクション"にいるときにページ離脱が発生すると、閲覧していたときのモードと逆のモードがブラウザに保存されてしまうため、今回は下記のようにクラス名で管理する方法を選択しました。

:root {
  color-scheme: light dark;
  --unique-white: #fff;
  --unique-black: #111;
  --unique-gray: #ddd;
  --unique-green: #4ade80;
  --bg-color: light-dark(#fff,#111);
  --text-color: light-dark(#111, #fff);
  --line-color: light-dark(#333,#ccc);
  --gray-main: light-dark(#666, #999);
  --gray-sub: light-dark(#bdbdbd,#444);
}

[data-theme="dark"] {
  color-scheme: dark;
}

[data-theme="dark"].is-reversed {
  color-scheme: light;
}

[data-theme="light"] {
  color-scheme: light;
}

[data-theme="light"].is-reversed {
  color-scheme: dark;
}

// コンテンツ用スタイル記載
.....

手順3(JavaScript)

テーマ切り替えはJavaScriptのclickイベント、"採用セクション"に入って色が逆転するアニメーションの適用は、先ほどご紹介したGSAPScrollTriggerを使って実装していきます。
ScrollTriggerには、markersという処理の発火範囲を可視化する機能があり、初学者でも発火タイミングを直感的に調整できます。

今回のアニメーションのターゲットとなる"採用セクション"(#recruit)を発火対象に設定し、
"採用セクション"の上端がビューポートの90%にきた時にテーマ反転を開始、下端がビューポートの70%の位置にきたときに元に戻します。

const themeStore = {
  THEMES: ['light', 'dark'],
  DEFAULT_THEME: 'light',
  CURRENT_THEME: '',

  getTheme() {
    if (!this.CURRENT_THEME) {
      const savedTheme = localStorage.getItem('theme')
      this.CURRENT_THEME = this.THEMES.includes(savedTheme) ? savedTheme : this.DEFAULT_THEME
    }
    return this.CURRENT_THEME
  },

  setTheme(theme) {
    if (!this.THEMES.includes(theme)) return
    this.CURRENT_THEME = theme
    document.documentElement.dataset.theme = theme
    localStorage.setItem('theme', theme)
  },

  toggleTheme() {
    const nextTheme = this.CURRENT_THEME === 'dark' ? 'light' : 'dark'
    this.setTheme(nextTheme)
    return nextTheme
  },
}

const toggleButton = document.getElementById('toggleButton')
const toggleTextLight = document.querySelector('.p-toggle__text[data-toggle="light"]')
const toggleTextDark = document.querySelector('.p-toggle__text[data-toggle="dark"]')
let originalTheme = themeStore.getTheme()

// UIの更新
function updateToggleUI(theme) {
  toggleTextLight.classList.toggle('is-active', theme === 'light')
  toggleTextDark.classList.toggle('is-active', theme === 'dark')
  toggleButton.classList.toggle('is-dark', theme === 'dark')
}

// 初期テーマ適用
themeStore.setTheme(themeStore.getTheme())
updateToggleUI(themeStore.getTheme())

// ボタンのクリックイベント
toggleButton.addEventListener('click', () => {
  const nextTheme = themeStore.toggleTheme()
  updateToggleUI(nextTheme)
  toggleButton.classList.toggle('is-active', nextTheme === 'dark')
  originalTheme = nextTheme
})

// ScrollTriggerによる一時的な反転テーマの制御
ScrollTrigger.create({
  trigger: document.getElementById('recruit'),
  start: 'top 90%',
  end: 'bottom 70%',
  onEnter: () => {
    originalTheme = themeStore.getTheme()
    document.documentElement.classList.add('is-reversed')
  },
  onLeave: () => {
    themeStore.setTheme(originalTheme)
    updateToggleUI(originalTheme)
    document.documentElement.classList.remove('is-reversed')
  },
  onEnterBack: () => {
    originalTheme = themeStore.getTheme()
    document.documentElement.classList.add('is-reversed')
  },
  onLeaveBack: () => {
    themeStore.setTheme(originalTheme)
    updateToggleUI(originalTheme)
    document.documentElement.classList.remove('is-reversed')
  },
})

以下は、今回設定した各オプションの説明です。

  1. onEnter:
    • 要素が監視範囲に入ったとき(スクロールダウン)に実行される処理。htmlタグにクラス名.is-reservedを付与し、一時的に現在のテーマとは逆のカラーに上書きします。
  2. onLeave:
    • 要素が監視範囲から外れたとき(スクロールダウン)に実行される処理。htmlタグのクラス名.is-reservedを外し、現在選択中のカラーに戻します。
  3. onEnterBack:
    • 要素が逆方向(スクロールアップ)にスクロールして再び範囲内に入ったときに実行される処理。要素が監視対象に入ったとき、htmlタグにクラス名.is-reservedを付与し、一時的に現在のテーマとは逆のカラーに上書きします。
  4. onLeaveBack:
    • 要素が逆方向(スクロールアップ)にスクロールして範囲外に出たときに実行される処理。要素が監視対象から外れたとき、htmlタグのクラス名.is-reservedを外し、現在選択中のカラーに戻します。

これで、トグルボタンでテーマを切り替えつつ、"採用セクション"が指定した位置に入った時に、一時的にカラーを逆転させることができます。
data-theme属性を通じてテーマ自体を変更することで、コンテンツ全体の色が動的に切り替わるため、静的に背景色を指定しておくよりも視覚的にインパクトを与えられるかと思います。

おまけ(Intersection Observerを使った実装)

今回は簡易的な実装方法としてScrollTriggerをご紹介しましたが、Web標準APIであるIntersectionObserverでも実装可能です。

以下、IntersectionObserverを使ったデモも作成しておりますので興味がある方は覗いてみてください。

参考文献