Scalable Theming With Styled-Components and TypeScript in React (Next.js)
Strongly typed theming with CSS-in-JS
Styled-components is a CSS-in-JS styling solution that makes it possible and easier for software developers to style React and React Native components using CSS.
CSS-in-JS is a styling method where you’d have to write all your CSS code in JavaScript. The JavaScript code is then compiled and parsed and the CSS code is generated and injected in a style tag into the DOM (Document Object Model). This styling method enables the abstraction of CSS to the component level itself, using JavaScript to describe styles in a declarative and maintainable way.
In this article, I'll show you how to set up scalable styled-components theming in your React and TypeScript applications. I'll be using Next.js as a case study.
Prerequisites
Before you begin this tutorial you'll need the following:
- Web Browser
- NPM or Yarn
- Styled-components Basics
- Prior knowledge of JavaScript, React, NextJs, and TypeScript
What is Theming?
Theming is a concept in CSS that allows you to customize all design aspects of your project, such as color, spacing, fonts, media query breakpoints, etc. It also makes it easier for users to customize web applications.
Now let's get started
Let's start by installing the nextjs typescript template. Open your terminal and cd to the directory where you want the project to be, and run the command below:
npx create-next-app@latest --typescript
# or
yarn create next-app --typescript
# or
pnpm create next-app --typescript
After the installation is complete:
cd into the project and Run npm run dev or yarn dev or pnpm dev
to start the development server on localhost:3000
Visit localhost:3000 to view your application
Edit pages/index.tsx and see the updated result in your browser
It's important to note that all the file extensions will be .tsx because we're using TypeScript instead of JavaScript.
Now it's time to install styled-components, its babel-plugin, and typescript declaration.
Installing Styled-components Dependencies
yarn add styled-components @types/styled-components babel-plugin-styled-components
# or
npm install add styled-components @types/styled-components babel-plugin-styled-components
After the installation is complete, we need to add the styled-components babel plugin to .babelrc (or babel.config.js) file at the top of our app. So let's do that by creating a babel file with the command below:
touch .babelrc
# or
touch babel.config.js
And then copying and pasting the code below in the .babelrc file we just created.
{
"presets": ["next/babel"],
"plugins": [
[
"styled-components",
{
"ssr": true,
"displayName": true,
"preprocess": false
}
]
]
}
What the above babel plugin and configuration does is it adds support for server-side rendering, minification of styles, and a nicer debugging experience.
PS: You can disable server-side rendering by setting the ssr to false (ssr: false)
Theme declaration, initialization, and configuration
Now that we have all the necessary dependencies installed, let's create the theme. At the top of our app, let's create the theme folder by running the command below:
mkdir theme
Now, inside the theme, let's create two files, globalStyles.ts and index.ts
touch globalStyles.ts index.ts
Inside the globalStyles.ts, we can add CSS styles that affect the entire app, such as CSS reset, CSS normalize, custom font declaration, etc.
The globalStyles.ts file would look like this:
import { createGlobalStyle } from 'styled-components'
export const GlobalStyles = createGlobalStyle`
body {
font-size: 16px;
scroll-behavior: smooth;
}
*, *::before, *::after {
box-sizing: border-box;
}
a {
text-decoration:none;
}
h1,h2,h3,h4,h5,h6 {
margin:0;
}
&::-webkit-scrollbar {
width: 3px;
}
`
Now let's open our index.ts file, then copy and paste the code below:
// for media query
const customMediaQuery = (maxWidth: number) =>
`@media (max-width: ${maxWidth}px)`;
// for passing of custom value
const customValue = (val: number) => `${val}px`
interface IMediaQueriesBreakpoints {
custom: (maxNumber: number) => string;
xs: string;
sm: string;
md: string;
lg: string;
xl: string;
xxl: string;
}
const media: IMediaQueriesBreakpoints = {
custom: customMediaQuery,
xs: customMediaQuery(330),
sm: customMediaQuery(592),
md: customMediaQuery(768),
lg: customMediaQuery(992),
xl: customMediaQuery(1024),
xxl: customMediaQuery(1200),
};
const colors = {
gradients: {
primary: {
100: "#EFEAF0",
200: "#E6F5F3",
300: "#E6EDF2",
},
secondary: {
100: "#E9D7E4",
200: "#EADCE1",
300: "#D6DEEA",
400: "#CFE4F5",
500: "#E7E8F6",
600: "#D0ECF7",
},
},
lightPink: "#F8F1EA",
black: "#000",
white: "#fff",
};
const fontSizes = {
sm: "12px",
md: "16px",
lg: "22px",
custom: customValue
};
const fontFamilies = {
clash: {
bold: "ClashDisplayBold, sans-serif",
extraLight: "ClashDisplayXtraLight, sans-serif",
light: "ClashDisplayLight, sans-serif",
medium: "ClashDisplayMedium, sans-serif",
regular: "ClashDisplayRegular, sans-serif",
semiBold: "ClashDisplaySemiBold, sans-serif",
},
manhope: {
bold: "ManropeBold, sans-serif",
extraBold: "ManropeExtraBold,sans-serif",
extraLight: "ManropeExtraLight, sans-serif",
light: "ManropeLight, sans-serif",
medium: "ManropeMedium, sans-serif",
regular: "ManropeRegular, sans-serif",
semiBold: "ManropeSemiBold, sans-serif",
},
};
const spacing = {
xs: "10px",
sm: "14px",
md: "22px",
custom: customValue
}
export const theme = {
colors,
fontSizes,
fontFamilies,
media,
};
Now that we've successfully created our theme, let's use typescript to super-charge our theme in order for us to get suggestions whenever we need anything from the theme.
mkdir @types
# and
touch styled.d.ts
Copy and paste the code below into the styled.d.ts:
import {} from 'styled-components'
// importing the theme we created
import theme from '../theme'
// global typescript declaration for the theme
declare module 'styled-components' {
type Theme = typeof theme
export interface DefaultTheme extends Theme {}
}
Adding the ThemeProvider to the application
First thing first, we need to create _document.tsx file in the pages folder i.e pages/_document.tsx
touch _document.tsx
Then copy and paste the code below into _document.tsx:
import Document, {
DocumentContext,
Head,
Html,
Main,
NextScript,
DocumentInitialProps
} from "next/document";
import { ServerStyleSheet } from "styled-components";
import React, { ReactElement } from "react";
export default class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext): Promise<DocumentInitialProps | any> {
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(): ReactElement {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
Now in the _app.tsx, let's add the styled-components ThemeProvider and the theme:
import { theme } from '../theme'
import { GlobalStyles } from '../theme/globalStyles'
import type { AppProps } from 'next/app'
import { ThemeProvider } from 'styled-components'
function MyApp({ Component, pageProps }: AppProps) {
return (
<ThemeProvider theme={defaultTheme}>
<GlobalStyles />
<Component {...pageProps} />
</ThemeProvider>
)
}
export default MyApp
Yaaay! and that's a wrap! We've successfully set up a scalable theme in our nextjs app. One more thing before we close this chapter, let's create a component to demonstrate how to use our awesome theme
So, let's create a button component:
import styled from 'styled-components'
import * as React from 'react'
const ButtonContainer = styled.button<{
variant: 'default' | 'secondary' | 'info' | 'outline';
disabled?: boolean;
}>`
width: 100%;
padding: ${({ theme }) => theme.spacing.custom(20)};
background: ${({ theme, variant }) =>
variant === 'default'
? theme.colors.secondary[400]
: variant === 'info'
? theme.colors.primary[100]
: variant === 'outline'
? 'inherit'
: theme.colors.secondary[200]};
color: ${({ theme, variant }) =>
variant === 'outline' ? theme.colors.gradients.primary?.[100] : theme.colors.white};
font-family: ${({ theme }) => theme.fontFamilies.clash.bold};
border: ${({ variant, theme }) =>
variant === 'outline' ? `1px solid ${theme.colors.lightPink}` : 'none'};
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: 0.3s ease-in-out;
:disabled {
background: ${({ theme }) => theme.colors.lightPink};
cursor: not-allowed;
}
&:hover {
filter: ${({ theme }) => `drop-shadow(5px 5px 5px ${theme.colors.gradients.primary.[300]}) invert(25%)`};
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;
}
// media query for small phone
${({ theme }) => theme.media.sm} {
height: ${({ theme }) => theme.spacing.custom(44)};
font-size: ${({ theme }) => theme?.fontSize?.custom(0.8)};
}
`;
//Typescript props type declaration
type IButton = {
text:string;
variant: 'default' | 'secondary' | 'info' | 'outline';
disabled?:boolean;
} & React.ComponentPropsWithoutRef<"button">;
//Button component
export const Button: React.FC<IButton> = (props) => {
return (
<ButtonContainer
{...props}
disabled={props.disabled}
variant={props.variant}
>
{props.text}
</ButtonContainer>
)
}
And that's a wrap! We have just created a button component with our theme. Go ahead and import it into the _app.tsx
and view it in the browser.
Conclusion
With CSS-in-JS, Software developers can now focus on writing business logic without having to worry about writing a bunch of CSS classes in a react component. I find it more interesting because it keeps my codebase clean and more readable.
Also, with the custom theme, Software developers now have full control over their styling and can customize it to suit their use cases. You can check out their official documentation to learn more about styled-components.