🌊

Next × Stitches × next-themesを使用してダークモード実装

2022/05/16に公開

Next.jsStitchesnext-themesを使用したダークモードの実装方法についてのまとめです。

例題としてMaterial DesignのButtonコンポーネントを参考に作成してみました!

lightモードで実装されたボタンコンポーネント

Stitchesでダークモード用のテーマを作成する

トークンを作成するためのconfigureファイルを作成する。

mkdir src/lib
touch src/lib/stitches.config.ts

Custom themingを参考にcreateStitchesメソッドを使用してtokensを作成する。

stitches.config.ts
// ベースとなるテーマ
export const { styled } = createStitches({
  theme: {
    colors: {
      text: 'hsl(0deg 0% 13% / 100%)',
      'text-inverted': 'hsl(0deg 0% 100% / 100%)',
      primary: 'hsl(210deg 79% 46% / 100%)',
    },
    space: { //省略  },
    font: { //省略  },    
  },
});


// ダークテーマ
export const darkTheme = createTheme({
  colors: {
    text: 'hsl(0deg 0% 100% / 100%)',
    'text-inverted': 'hsl(0deg 0% 0% / 100%)',
    primary: 'hsl(207deg 90% 77% / 100%)',
  },
});
text-invertedについて

text-invertedはvariantがcontainedの際に使用するプロパティとしています。
Material UIcontainedボタンがイメージです。通常時とダークモード時のbackground-colorに対して反転したテキストカラーが必要となるためです(もっとスマートな方法がありましたらぜひ教えていただきたいです...)

Stitchesを使ってButtonコンポーネントを作成する

例として MUIのようにcontained(背景色あり)、text(テキストラベルのみ)outlined(枠線のみ)の3パターンの作成をします。

3種類のボタンバリエーション、text、contained、outlined

Button.tsx
import { styled } from '@/lib/stitches.config';

const Button = styled('button', {
  color: 'White',
  background: 'hsl(0deg 0% 100%)',
  border: 'none',
  paddingInline: 16,
  paddingBlock: 8,
  borderRadius: '$2',
  fontWeight: 500,
  fontSize: 14,
  textTransform: 'uppercase',
  userSelect: 'none',
  cursor: 'pointer',
});

ベースとなるコンポーネントのスタイルを作成しました。activeやhover時のスタイルはサンプルでは割愛します🙏

Variantsでスタイルにバリエーションを与える

StitchesのVariantsでButtonコンポーネントに対してスタイルにバリエーションを与えます。

Button.tsx
const Button = styled('button', {
  color: 'White',
  background: 'hsl(0deg 0% 100%)',
  border: 'none',
  paddingInline: 16,
  paddingBlock: 8,
  borderRadius: '$2',
  fontWeight: 500,
  fontSize: 14,
  textTransform: 'uppercase',
  userSelect: 'none',
  cursor: 'pointer',

  // 下記追加
  variants: {
    variant: {
      contained: {
        background: '$primary',
        color: '$text-inverted',
      },
      text: {
        background: 'transparent',
        color: '$primary',
      },
      outlined: {
        background: 'transparent',
        color: '$primary',
        border: '1px solid',
        borderColor: '$primary',
        paddingInline: 15,
        paddingBlock: 7,
      },
    },
  },
});

variants内部にvariantオブジェクトを作成。さらにcontainedtextとそれぞれのスタイルを定義します。

$primaryのように$マークを頭に付与することで、createStitchesメソッドを使用して設定したトークンのスタイルを充てることができます。通常用とダークモード用でそれぞれ$primaryカラーを作成したので、テーマ切り替えに伴い参照するスタイルが変わります。

以上でButtonコンポーネント側のvariantpropでそれぞれのスタイルを呼び出すことができます。

Button.tsx
<Button variant="contained" onClick={toggleTheme}>
    Contained
</Button>
<Button variant="text" onClick={toggleTheme}>
    Text
</Button>
<Button variant="outlined" onClick={toggleTheme}>
    Outlined
</Button>

next-themesを設定する

yarn add next-themes

ThemeProviderを追加し下記のように設定します 参考記事

_app.tsx
function MyApp({ Component, pageProps }: AppProps) {  
  return (
    <ThemeProvider
      attribute="class"
      defaultTheme="system"
      value={{
        light: 'light',
        dark: darkTheme.className,
      }}
    >
      <Component {...pageProps} />
    </ThemeProvider>
  );
}
export default MyApp;

以上、これだけでダークモードが適用されます!

テーマが切り替わるか確認

themeがclient側でマウントされるまではundefinedとなります。その場合、UIをレンダリングしようとするとhydration mismatch errorが発生するので、下記の処理を追加します。
Avoid Hydration Mismatch

Home.tsx
const Home: NextPage = (): JSX.Element | null => {
  const [mounted, setMounted] = useState(false);
  const { theme, setTheme } = useTheme();

 /** client側でmountを確認 **/
  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) {
    return null;
  }

  const toggleTheme = () => {
    setTheme(theme === 'light' ? 'dark' : 'light');
  };

  return (
    <Container>
      <Main>
        <Box css={{ padding: 16 }}>
          <p>現在のテーマ: {theme === 'dark' ? 'DARK' : 'LIGHT'}</p>

          <Box css={{ marginTop: 8 }}>
            <Button variant="contained" onClick={toggleTheme}>
              Contained
            </Button>
            <Button variant="text" onClick={toggleTheme}>
              Text
            </Button>
            <Button variant="outlined" onClick={toggleTheme}>
              Outlined
            </Button>
          </Box>
        </Box>
      </Main>
    </Container>
  );
};

export default Home;

const Container = styled('div', {
  display: 'flex',
  flexDirection: 'column',
  justifyContent: 'center',
  minHeight: '100vh',
});
const Main = styled('main', {
  flex: 1,
});
const Box = styled('div');

ダークモード時のスタイルが適用されました👏
darkモードで実装されたボタンコンポーネント

記事を読んでくださった方、最後までありがとうございました!
駆け足で書きましたので、不足、不明な点等ございましたらぜひコメントをください...。

Happy hacking 🎉

Discussion