Next.jsで外観モード切替機能を実装してみる

  • Web
  • NextJS

はじめに

サードパーティライブラリを使わずに、Next.js製のサイトでダークモード対応してみようという回です。

use-dark-modeといったライブラリもありますが、勉強がてらスクラッチで作ってみようと思います。

実装🛠

プロジェクトを用意

# Default starter appで作成
npx create-next-app nextjs-workspace
cd nextjs-workspace

ディレクトリを用意

mkdir styles
mkdir components

グローバルスタイルを外部ファイルへ書き出し

デフォルトだとpages/index.jsがあり、ファイル中にグローバルスタイルが定義されていますので、それをstyles/global.cssでファイルに書き出そうと思います。

この作業は必須ではないのですが、グローバルなものはファイルに書き出しておきたいためです。

/* styles/global.css */
html, body {
  padding: 0;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
    Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue,
    sans-serif;
}
* { box-sizing: border-box; }

外部ファイルに書き出したグローバルスタイルはpages/_app.jsで読み込むようにします。

/* pages/_app.js */
import App from 'next/app'
// ↓ 読み込み
import '../styles/global.css'
class MyApp extends App {
  render() {
    const { Component, pageProps } = this.props
    return (<><Component {...pageProps} /></>)
  }
}
export default MyApp

色のカスタムプロパティを定義

カスタムプロパティはCSSの変数です。IE11が非対応ですが、今回は気にせず使っていきましょう。気になる方はPostCSSの設定(postcss.config.jsに記述)でフォールバックを対応させることも可能です。

さて、作業内容はpages/index.js中にCSS(styled-jsx)が書かれていますので、そこで利用されているカラーコードをカスタムプロパティに置き換えることをします。

まずは、カスタムプロパティはstyles/global.cssの冒頭に追記します。

/* styles/global.css */
:root {
  --color-text: #000;
  --color-bg: #fff;
  --color-code-bg: #fafafa;
  --color-border: #eaeaea;
  --color-link: #0070f3;
}

pages/index.jsの色コード部分をvar(--color-border)などで置換します。

/* pages/index.js */
import Head from 'next/head'
export default function Home() {
  return (
    <div className="container">
      <Head>
        <title>Create Next App</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>
      /* 一部抜粋しています */
      <style jsx>{`
        footer {
          width: 100%;
          height: 100px;
          border-top: 1px solid var(--color-border);
          display: flex;
          justify-content: center;
          align-items: center;
        }
      `}</style>
    </div>
  )
}

カラーモード切り替えのボタンを用意

ボタンのコンポーネントを作ろうと思います。以下の内容でcomponents/switching-theme.jsを作成してください。

/**
  * ローカルストレージにカラーモードの識別子: dark/lightを登録する.
  * htmlタグにdata-theme属性を付与して、見た目の切り替えを実現する.
  */
export default function SwitchingTheme() {
  const setTheme = (newTheme) => {
    const theme = !(newTheme === 'dark' || newTheme === 'light') ? 'light' : newTheme
    // ここではdata-themeとしていますが任意の名前でOKで大丈夫です.
    document.documentElement.setAttribute('data-theme', theme)
    window.localStorage.setItem('theme', theme)
  }
  const getTheme = () => {
    const currentTheme = window.localStorage.getItem('theme')
    if (currentTheme) {
      return currentTheme
    }
    // ローカルストレージにデータ未登録の場合は、クライアントのシステムの外観モードで決定
    // (メディアクエリの結果から判定)
    const query = window.matchMedia('(prefers-color-scheme: dark)')
    return (query.matches ? 'dark' : 'light')
  }
  const changeTheme = (e) => {
    const currentTheme = getTheme()
    const newTheme = (currentTheme === 'dark') ? 'light' : 'dark'
    setTheme(newTheme)
  }
  setTheme(getTheme())

  return (
    <>
      <button type="button" onClick={changeTheme}>Switching Theme</button>
      <style jsx>{`
        button {
          border: 0;
          cursor: pointer;
          outline: none;
          padding: 10px 15px;
          appearance: none;
          background-color: var(--color-link);
          border-radius: 5px;
          color: var(--color-button-text);
        }
      `}</style>
    </>
  )
}

ボタンのテキストの色を表すカスタムプロパティを追加したので、CSSファイルにも反映が必要です。また、ダークモードの場合のカスタムプロパティの内容も設定しましょう。

↓ 最終的なstyles/global.cssの内容です。

:root {
  --color-text: #000;
  --color-button-text: #fff;
  --color-bg: #fff;
  --color-code-bg: #fafafa;
  --color-border: #eaeaea;
  --color-link: #0070f3;
}
/* 追記 */
:root[data-theme=dark] {
  --color-text: #fff;
  --color-bg: #333;
  --color-code-bg: #2d2d2d;
  --color-border: #dfdfdf;
  --color-link: #f38300;
}
html, body {
  padding: 0;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
    Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue,
    sans-serif;
}
/* 追記 */
body {
  background-color: var(--color-bg);
  color: var(--color-text);
}
* { box-sizing: border-box; }

最後にボタンのコンポーネントをページに配置します。

ここで注意しておきたいことは、カラーモード切り替えはクライアントサイドだけの処理で、サーバーサイドではwindowがない点です。(先ほどのコンポーネント中のコードを見てみましょう)この点はNext.jsだけでなく、他のNuxt.jsでも同様でSSRで動かす際は気にしておかないといけないところです。

/* pages/index.js */
import Head from 'next/head'
import dynamic from 'next/dynamic'

export default function Home() {
  // 動的インポートする
  const DynamicComponent = dynamic(() => import('../components/switching-theme'))
  return (
    <main>
      // ※主要な箇所のみ抜粋しています
      <DynamicComponent />
    </main>
  )

動作確認

npm run dev

実際にページにアクセスしてみましょう。以下のような画面が現れたでしょうか。

おわりに

スクラッチでも特に問題なく実装できました。

カラーモードの切り替えを考慮したCSS設計をしないといけない点は地味に大変かなと思いました。ライトモードとダークモードの両方を確認して、どちらでも問題なく見れるように配色を考えなければならないですからね。。。

この記事を共有

アバター

K.Utsunomiya
男・20代
主にWebフロントエンド技術と気になった音楽について投稿していきます。
最近ハマっていることは、クロスバイクで走ることとジムでの運動です。
詳しいプロフィール

© 2020–2021 コレ棚