styled-components でダークモード

styled-components でダークモードとライトモードを実現する方法について順を追ったメモ。

ライトテーマとダークテーマの切り替え

CSS変数とGlobalStyle

ダークモード又はライトモードとして設定された色をCSSで使うためには次のようにCSS変数を介しておこないます。

color: var(–main-fg-color);

それぞれの色の設定は、次のように createGlobalStyleというヘルパー関数を用いて、グローバルに使用可能なCSSを設定できる特別GlobalStyleを生成します。

// src\styled\Global.js

import { createGlobalStyle } from 'styled-components';

const isDarkMode = false; // この値は後でコードから設定するとして・・・

export const GlobalStyle = createGlobalStyle`
  :root {
    --main-bg-gradient1: ${isDarkMode ? '#3D0529' : '#d4d3dd'};
    --main-bg-gradient2: ${isDarkMode ? '#7B103E' : '#efefbb'};
    --main-fg-color: ${isDarkMode ? '#fff' : '#333'};
    --card-bg-color: ${isDarkMode ? '#15232D' : '#fff'};
    --card-textarer-bg-color: ${isDarkMode ? '#193549' : '#eee'};
  }

この設定を使えるようにするために、例えば次のようにGlobalStyleコンポーネントを使います。

// src\App.jsx


:


import styled from 'styled-components';
import { GlobalStyle } from './styled/Global';

:

function App() {
  return (
    <Router>
      <GlobalStyle />
      :

なんか、ダークモード/ライトモードの切り替えならこれでも十分な気がします。

ThemeProviderを使う方法

こちらは先程のGlobalStyleに加えて、ThemeProviderを使う方法です。ダークモードとライトモードのそれぞれの色を先程はGlobalStyleから得ていましたが、これをテーマ(ダークテーマとライトテーマ)から得るようにします。つまり、色の設定を各テーマに集める方法です。テーマが2種類でなく、もっと沢山あるような場合は扱いやすくなるとは思います。

まずはテーマごとの色設定を別ファイルに移動します。

// src\styled\Themes.js

const sharedStyles = {
  // 全テーマ共通設定
  buttonBgColor: '#34d5df',
  buttonBgHoverColor: '#cb43f5',
};

export const darkTheme = {
  mainBgGradientColor1: '#3D0529',
  mainBgGradientColor2: '#7B103E',
  mainFgColor: '#fff',
  cardBgColor: '#15232D',
  cardTextarerBgColor: '#193549',
  ...sharedStyles,
};

export const lightTheme = {
  mainBgGradientColor1: '#d4d3dd',
  mainBgGradientColor2: '#efefbb',
  mainFgColor: '#333',
  cardBgColor: '#fff',
  cardTextarerBgColor: '#eee',
  ...sharedStyles,
};

そして、前述の Global.js は次のように変更します。変数 isDarkMode は不要になりました。

// src\styled\Global.js


import { createGlobalStyle } from 'styled-components';

export const GlobalStyle = createGlobalStyle`
  :root {
    --main-bg-gradient-color1: ${(props) => props.theme.mainBgGradientColor1};
    --main-bg-gradient-color2: ${(props) => props.theme.mainBgGradientColor2};
    --main-fg-color: ${(props) => props.theme.mainFgColor};
    --card-bg-color: ${(props) => props.theme.cardBgColor};
    --card-textarer-bg-color: ${(props) => props.theme.cardTextarerBgColor};
  }

前述の App.js では、ThemeProvider で囲み、テーマの初期値を設定します。

// src\App.jsx

:

import { ThemeProvider } from 'styled-components';
import { GlobalStyle } from './styled/Global';
import { lightTheme, darkTheme } from './styled/Themes';

:

function App() {
  const theme = 'light'; // この値は後でコードから設定するとして・・・
  const currentTheme = theme === 'light' ? lightTheme : darkTheme;

  return (
    <Router>
      <ThemeProvider theme={currentTheme}>

        <GlobalStyle />
        :
    </Router>
  );
}

テーマ切り替えのフックとその使用

テーマを切り替えるため、次のようなフックを作成します。

// src\hooks\useTheme.js

import { useState } from 'react';
export default () => {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    if (theme === 'light') {
      setTheme('dark');
    } else {
      setTheme('light');
    }
  };
  return [theme, toggleTheme];
};

ただし、これはコンポーネント間で共有されるステート(グローバルステート)ではありませんので、下のように toggleTheme props App から Navbar コンポーネントに渡します。

// src\App.jsx


:

import useTheme from './hooks/useTheme';


:

function App() {
  const [theme, toggleTheme] = useTheme();   <--- ★このコンポーネントだけのステート
  const currentTheme = theme === 'light' ? lightTheme : darkTheme;

  return (
    <Router>
      <ThemeProvider theme={currentTheme}>
        <GlobalStyle />
        <StyledApp>
          <Navber toggleTheme={toggleTheme} /> <--- ★下位コンポーネントに関数を渡す

Navbarコンポーネントにテーマ切り替えボタンを次のように追加しました。

// src\components\Navbar.jsx

:

import useTheme from '../hooks/useTheme';

:

const Navbar = ({toggleTheme}) => {
  :
  
  return (
  
  :
        <button onClick={toggleTheme}>テーマ切替</button>

        :

テーマ選択状態の保存

現在はテーマを切り替えても、ページをリロードすると初期状態に戻ってしまいます。後日、アクセスした場合に前に選択したテーマで表示されるように状態をローカルストレージに保存するようにします。

import { useState, useEffect } from 'react';
export default () => {
  const [theme, setTheme] = useState('light');

  useEffect(() => {
    const localStorageTheme = localStorage.getItem('theme');
    setTheme(localStorageTheme || 'light');
  }, []);

  const toggleTheme = () => {
    if (theme === 'light') {
      setTheme('dark');
      localStorage.setItem('theme', 'dark');
    } else {
      setTheme('light');
      localStorage.setItem('theme', 'light');
    }
  };
  return [theme, toggleTheme];
};