Dark Mode vs Light Mode?
Let your users decide. Provide the option to switch seamlessly between themes with styled-components
and mobx
.
Getting Started
- Installing dependencies
- Creating the theme constants
- Configuring styled-components with Next.js
- Adding styled-components' server-side functionality
- Persisting data
- Creating a mobx store
- Create a global style
- Creating the StyleProvider
- Adding types to the theme
- Making sure it works
1. Install
- The dependencies:
yarn add styled-components mobx mobx-react
- The dev-dependencies:
yarn add -D @types/styled-components
2. Create a file for the theme constants
themes.constants.ts
In this file, we're going to define the themes and other relavant constants.
- Colours
const COLOURS = {
black: '#000000',
white: '#FFFFFF'
}
- Dark theme
const DARK_THEME = {
name: 'dark',
background: COLOURS.black,
textPrimary: COLOURS.white
}
- Light theme
const LIGHT_THEME = {
name: 'light',
background: COLOURS.white,
textPrimary: COLOURS.black
}
- Export the Default theme
export const DEFAULT_THEME = 'dark'
- Export the themes
export const THEMES = {
dark: DARK_THEME,
light: LIGHT_THEME,
}
3. Add styled-components to the next.config.js
file
In the nextConfig object, add the key-pair values:
compiler: {
styledComponents: true,
},
4. Add ServerStyleSheet
to _document.tsx
Add the getInitialProps
method to the MyDocument
class shown below.
If you haven't already created _document.tsx
, add it to your pages
folder. And paste the following:
import Document, {
Html,
Head,
Main,
NextScript,
DocumentContext,
} from 'next/document';
import { ServerStyleSheet } from 'styled-components';
class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
sheet.collectStyles(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: [
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>,
],
};
} finally {
sheet.seal();
}
}
render() {
return (
<Html>
<Head>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
5. Create utility functions to allow persisting data with localStorage
utils.ts
export const setStorage = (key: string, value: unknown) => {
window.localStorage.setItem(key, JSON.stringify(value));
};
export const getStorage = (key: string) => {
const value = window.localStorage.getItem(key);
if (value) {
return JSON.parse(value);
}
};
6. Create a UI Store with mobx
uiStore.ts
import { action, makeAutoObservable, observable } from 'mobx';
import { setStorage } from './utils';
type Themes = 'dark' | 'light';
class UiStore {
@observable
private _theme: Themes = 'light';
@observable
private _initializing: boolean = true;
constructor() {
makeAutoObservable(this);
}
get theme() {
return this._theme;
}
get initializing() {
return this._initializing;
}
@action
toggleTheme() {
this._theme = this._theme === 'light' ? 'dark' : 'light';
setStorage('theme', this._theme);
}
@action
changeTheme(nameOfTheme: Themes) {
this._theme = nameOfTheme;
}
@action
finishInitializing() {
this._initializing = false;
}
}
export const uiStore = new UiStore();
7. Create a global style with styled-components
We'll soon be able to access the theme in the styled-components in the following manner.
global-styles.ts
export const GlobalStyle = createGlobalStyle`
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
background: ${({ theme }) => theme.colors.body};
}
`;
8. Creating the StyleProvider
style-provider.tsx
import { observer } from 'mobx-react';
import { useEffect, ReactNode } from 'react';
import { ThemeProvider } from 'styled-components';
import { uiStore } from './uiStore';
import { DEFAULT_THEME, THEMES } from './theme.constants';
import { GlobalStyle } from './global-styles`
import { getStorage, setStorage } from './utils';
interface OnlyChildren {
children: ReactNode
}
const StyleProviderComponent = (props: OnlyChildren) => {
const { children } = props;
const { theme } = uiStore;
useEffect(() => {
if (!getStorage('theme')) {
setStorage('theme', DEFAULT_THEME);
}
const storageThemeName = getStorage('theme');
uiStore.changeTheme(storageThemeName);
uiStore.finishInitializing();
}, []);
return (
<ThemeProvider theme={THEMES[theme]}>
<GlobalStyle />
{children}
</ThemeProvider>
);
};
export const StyleProvider = observer(StyleProviderComponent);
9. Add typing to the theme
default-theme.d.ts
import 'styled-components';
declare module 'styled-components' {
export interface DefaultTheme {
name: string;
colors: {
primary: string;
textPrimary: string;
};
}
}
10. Wrap the provider around pages/_app.tsx
import type { AppProps } from 'next/app';
import { StyleProvider } from './style-provider';
function MyApp({ Component, pageProps }: AppProps) {
return (
<StyleProvider>
<Component {...pageProps} />
</StyleProvider>
);
}
export default MyApp;
Test that it works in pages/index.tsx
import type { NextPage } from 'next';
import Head from 'next/head';
import { uiStore } from './uiStore';
const Home: NextPage = () => {
const { initializing } = uiStore;
if (!initializing) {
return <h1>Loading...</h1>;
}
return (
<>
<Head>
<title>
Adding themes to Next.js with styled-components, mobx, and typescript
</title>
<meta name='description' content='Tutorial by Zach Nugent' />
<link rel='icon' href='/favicon.ico' />
</Head>
<button onClick={() => uiStore.toggleTheme()}>Switch theme</button>
</>
)
}
export default Home
You can access the theme in styled-components by adding a callback function (ex: ${({ theme }) => theme.colors.textPrimary}
) in between the back ticks.
Example:
const Button = styled.button`
background: ${({ theme }) => theme.colors.body};
border: 1px solid ${({ theme }) => theme.colors.accent};
color: ${({ theme }) => theme.colors.textPrimary};
`
Thanks for reading 🙂
Top comments (0)