Type-Safe Styled-Components Theming for React.js and Next.js πŸ’…

We all know and love styled-components and Typescript, but when a library is not typed, it can be difficult to use and seem very hacky. You can and should add the types from @types/styled-components but it doesn't seem to work very well with custom themes. This post will help you with that.

If you tried using styled-components with custom theming, you must've had errors like this:
Styled-Components-Typescript: Property 'backgroundColor' does not exist on type 'DefaultTheme', which is a very common error if you're using a custom theme with Typescript and apparently they don't maintain the types.

There are at least two solutions to this problem:

  • The official way, which is very manual
  • The easy way, which everything works automatically

The official way (Very manual)

If you go to the styled-components documentation, you'll see a super small section about Typescript in there, which is fine until you need to make a lot of changes to your theme.

The way it's done in the official documentation to make your theme type-safe is:

  • Create a declarations file (styled.d.ts)
  • Import the DefaultTheme from styled-components
  • Extend it with your custom theme

Something like this:

// import original module declarations
import "styled-components";

// and extend them!
declare module "styled-components" {
  export interface DefaultTheme {
    borderRadius: string;

    colors: {
      main: string;
      secondary: string;
    };
  }
}

// source: https://styled-components.com/docs/api#create-a-declarations-file

Then you create a custom theme and reference the DefaultTheme interface you just created, like this:

// myTheme.ts
import { DefaultTheme } from "styled-components";

const myTheme: DefaultTheme = {
  borderRadius: "5px",

  colors: {
    main: "cyan",
    secondary: "magenta",
  },
};

export { myTheme };

// source: https://styled-components.com/docs/api#create-a-theme

It works fine, the problem is that everytime you need to add/remove something from your theme, you'll need to also update the declarations file.

The easy way (very automatic)

The way I do is to just create a custom theme file and then automatically create an interface for it. This way you don't need to update the declarations file everytime you add something to your theme.

The steps are:

  • Create a custom theme file
  • Create a declarations file (styled.d.ts)

Create your theme

// myTheme.ts
export const myTheme = {
  borderRadius: "5px",

  colors: {
    main: "cyan",
    secondary: "magenta",
  },
};

Create the declarations file

Here's the fun part. When you create the declarations file, you'll need to import the DefaultTheme from styled-components like before, but instead of creating a new interface, you'll just extend the type of your theme (myTheme) with the DefaultTheme.

// styled.d.ts
import "styled-components";
import { myTheme } from "./theme";

declare module "styled-components" {
  type MyTheme = typeof myTheme;

  interface DefaultTheme extends MyTheme {}
}

I know it's not the most elegant way to do it, but it works.

Using the theme

I know it's not the purpose of this post to explain how to create and use the theme, but I'll do it anyway.

You're probably using React.js, so you have an App.tsx or _app.jsx in Next.js. In that file you just need to create a ThemeProvider and pass your theme to it. Something like this:

// With React.js
import { ThemeProvider } from "styled-components";
import { myTheme } from "./theme";

function App() {
  return (
    <ThemeProvider theme={myTheme}>
      <h1>Hello World!</h1>
    </ThemeProvider>
  );
}

export default App;

And for Next.js people, you just need to change a little bit the default _app.tsx file they give you.

// With Next.js
import { ThemeProvider } from "styled-components";
import type { AppProps } from "next/app";
import { myTheme } from "./theme";

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <ThemeProvider theme={myTheme}>
      <Component {...pageProps} />
    </ThemeProvider>
  );
}

With that done, you can easily access your theme in your components.

// Box.tsx
import styled from "styled-components";

const BoxContainer = styled.div`
  display: flex;
  border-radius: ${(props) => props.theme.borderRadius};
  color: ${(props) => props.theme.colors.main};
  background-color: ${(props) => props.theme.colors.secondary};
`;

const Box = () => <BoxContainer>Hello World!</BoxContainer>;

Or a more direct approach with object destructuring:

// Box.tsx
import styled from "styled-components";

const BoxContainer = styled.div`
  display: flex;
  border-radius: ${({ theme }) => theme.borderRadius};
  color: ${({ theme }) => theme.colors.main};
  background-color: ${({ theme }) => theme.colors.secondary};
`;

const Box = () => <BoxContainer>Hello World!</BoxContainer>;

Here you can see the autocompletion of the theme in your components without the need to update the declarations file.

I hope this post helped you with your styled-components and typescript problems. If you have any questions, please don't hesitate to ask me in the comments. I'm always happy to help.

26