DEV Community

Cover image for Adding themes to Next.js with styled-components, mobx, and typescript
Zach Nugent
Zach Nugent

Posted on

Adding themes to Next.js with styled-components, mobx, and typescript

Dark Mode vs Light Mode?

Let your users decide. Provide the option to switch seamlessly between themes with styled-components and mobx.

Getting Started

  1. Installing dependencies
  2. Creating the theme constants
  3. Configuring styled-components with Next.js
  4. Adding styled-components' server-side functionality
  5. Persisting data
  6. Creating a mobx store
  7. Create a global style
  8. Creating the StyleProvider
  9. Adding types to the theme
  10. 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'
}
Enter fullscreen mode Exit fullscreen mode
  • Dark theme
const DARK_THEME = {
  name: 'dark',
  background: COLOURS.black,
  textPrimary: COLOURS.white
}
Enter fullscreen mode Exit fullscreen mode
  • Light theme
const LIGHT_THEME = {
  name: 'light',
  background: COLOURS.white,
  textPrimary: COLOURS.black
}
Enter fullscreen mode Exit fullscreen mode
  • Export the Default theme
export const DEFAULT_THEME = 'dark'
Enter fullscreen mode Exit fullscreen mode
  • Export the themes
export const THEMES = {
  dark: DARK_THEME,
  light: LIGHT_THEME,
}
Enter fullscreen mode Exit fullscreen mode

3. Add styled-components to the next.config.js file

In the nextConfig object, add the key-pair values:

  compiler: {
    styledComponents: true,
  },
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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);
  }
};

Enter fullscreen mode Exit fullscreen mode

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();

Enter fullscreen mode Exit fullscreen mode

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};
}

`;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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};
`
Enter fullscreen mode Exit fullscreen mode

Thanks for reading πŸ™‚

Top comments (0)