Next × Stitches × next-themesを使用してダークモード実装
Next.jsにStitchesとnext-themesを使用したダークモードの実装方法についてのまとめです。
例題としてMaterial DesignのButtonコンポーネントを参考に作成してみました!
Stitchesでダークモード用のテーマを作成する
トークンを作成するためのconfigureファイルを作成する。
mkdir src/lib
touch src/lib/stitches.config.ts
Custom themingを参考にcreateStitches
メソッドを使用してtokensを作成する。
// ベースとなるテーマ
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 UIのcontained
ボタンがイメージです。通常時とダークモード時のbackground-color
に対して反転したテキストカラーが必要となるためです(もっとスマートな方法がありましたらぜひ教えていただきたいです...)
Stitchesを使ってButtonコンポーネントを作成する
例として MUIのようにcontained
(背景色あり)、text
(テキストラベルのみ)outlined
(枠線のみ)の3パターンの作成をします。
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コンポーネントに対してスタイルにバリエーションを与えます。
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
オブジェクトを作成。さらにcontained
、text
とそれぞれのスタイルを定義します。
$primary
のように$マークを頭に付与することで、createStitches
メソッドを使用して設定したトークンのスタイルを充てることができます。通常用とダークモード用でそれぞれ$primary
カラーを作成したので、テーマ切り替えに伴い参照するスタイルが変わります。
以上でButton
コンポーネント側のvariant
propでそれぞれのスタイルを呼び出すことができます。
<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
を追加し下記のように設定します 参考記事
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
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');
ダークモード時のスタイルが適用されました👏
記事を読んでくださった方、最後までありがとうございました!
駆け足で書きましたので、不足、不明な点等ございましたらぜひコメントをください...。
Happy hacking 🎉
Discussion