CSS-in-JS in Next.js
Introduction
CSS-in-JS is a styling approach where you write CSS directly in your JavaScript code. This paradigm offers component-scoped styles that can respond to component props, making your styling more dynamic and tightly coupled with your components. In Next.js applications, CSS-in-JS libraries provide an alternative to traditional CSS, CSS Modules, or Sass.
In this guide, we'll explore how to use popular CSS-in-JS libraries with Next.js, understand their benefits and drawbacks, and see practical examples of implementing them in your projects.
Why Use CSS-in-JS in Next.js?
CSS-in-JS offers several advantages for Next.js applications:
- Component-scoped styles: Styles are isolated to specific components, preventing class name collisions
- Dynamic styling: Style properties can change based on component props or state
- No need for class naming conventions: Reduces the mental overhead of naming CSS classes
- Co-location: Keeps styles close to the components they affect
- JavaScript features in CSS: Use variables, functions, and other JavaScript features in your styles
Popular CSS-in-JS Libraries for Next.js
1. styled-components
styled-components is one of the most popular CSS-in-JS libraries, allowing you to write actual CSS in your JavaScript files.
2. Emotion
Emotion is a flexible CSS-in-JS library that lets you write styles with JavaScript, similar to styled-components but with some different features and optimizations.
Setting Up CSS-in-JS in Next.js
Let's see how to set up and use these libraries in a Next.js project.
Setting up styled-components
- First, install styled-components:
npm install styled-components
# or
yarn add styled-components
- To ensure server-side rendering works correctly with styled-components, you need to create a custom
_document.js
file:
// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheet } from 'styled-components';
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
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 />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
Setting up Emotion
- Install Emotion:
npm install @emotion/react @emotion/styled
# or
yarn add @emotion/react @emotion/styled
- For Next.js 12+, Emotion works out of the box without additional configuration for server-side rendering.
Using styled-components in Next.js
Let's create a simple Button component using styled-components:
// components/Button.js
import styled from 'styled-components';
const Button = styled.button`
background-color: ${props => props.primary ? '#0070f3' : 'white'};
color: ${props => props.primary ? 'white' : '#0070f3'};
font-size: 16px;
padding: 10px 20px;
border-radius: 4px;
border: 1px solid #0070f3;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background-color: ${props => props.primary ? '#005cc5' : '#f8f9fa'};
}
&:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(0, 112, 243, 0.3);
}
`;
export default Button;
Using this Button component in a page:
// pages/index.js
import Button from '../components/Button';
export default function Home() {
return (
<div>
<h1>Welcome to My App</h1>
<Button primary>Primary Button</Button>
<Button>Secondary Button</Button>
</div>
);
}
Output
When rendered, the primary button will have a blue background with white text, while the secondary button will have a white background with blue text. Both buttons will have hover and focus states with smooth transitions.
Using Emotion in Next.js
Let's create the same Button component using Emotion:
// components/Button.js
import styled from '@emotion/styled';
const Button = styled.button`
background-color: ${props => props.primary ? '#0070f3' : 'white'};
color: ${props => props.primary ? 'white' : '#0070f3'};
font-size: 16px;
padding: 10px 20px;
border-radius: 4px;
border: 1px solid #0070f3;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background-color: ${props => props.primary ? '#005cc5' : '#f8f9fa'};
}
&:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(0, 112, 243, 0.3);
}
`;
export default Button;
The usage in a page is the same as with styled-components:
// pages/index.js
import Button from '../components/Button';
export default function Home() {
return (
<div>
<h1>Welcome to My App</h1>
<Button primary>Primary Button</Button>
<Button>Secondary Button</Button>
</div>
);
}
Advanced CSS-in-JS Features
1. Theming
Both styled-components and Emotion support themes. Here's how to set up a theme with styled-components:
// pages/_app.js
import { ThemeProvider } from 'styled-components';
import '../styles/globals.css';
const theme = {
colors: {
primary: '#0070f3',
secondary: '#ff4081',
background: '#ffffff',
text: '#333333',
},
fonts: {
body: 'system-ui, sans-serif',
heading: 'Georgia, serif',
},
fontSizes: {
small: '14px',
medium: '16px',
large: '18px',
},
};
function MyApp({ Component, pageProps }) {
return (
<ThemeProvider theme={theme}>
<Component {...pageProps} />
</ThemeProvider>
);
}
export default MyApp;
Using the theme in a component:
// components/ThemedButton.js
import styled from 'styled-components';
const ThemedButton = styled.button`
background-color: ${props => props.primary ? props.theme.colors.primary : 'white'};
color: ${props => props.primary ? 'white' : props.theme.colors.primary};
font-family: ${props => props.theme.fonts.body};
font-size: ${props => props.theme.fontSizes.medium};
padding: 10px 20px;
border-radius: 4px;
border: 1px solid ${props => props.theme.colors.primary};
cursor: pointer;
`;
export default ThemedButton;
2. Global Styles
You can define global styles using the createGlobalStyle
function in styled-components:
// components/GlobalStyles.js
import { createGlobalStyle } from 'styled-components';
const GlobalStyles = createGlobalStyle`
body {
margin: 0;
padding: 0;
font-family: ${props => props.theme.fonts.body};
color: ${props => props.theme.colors.text};
background-color: ${props => props.theme.colors.background};
}
a {
color: ${props => props.theme.colors.primary};
text-decoration: none;
}
`;
export default GlobalStyles;
Then include it in your _app.js
:
// pages/_app.js
import { ThemeProvider } from 'styled-components';
import GlobalStyles from '../components/GlobalStyles';
const theme = {
// Theme definition as before
};
function MyApp({ Component, pageProps }) {
return (
<ThemeProvider theme={theme}>
<GlobalStyles />
<Component {...pageProps} />
</ThemeProvider>
);
}
export default MyApp;
3. Extending Styles
You can extend existing styled components to create new variations:
// components/ExtendedButton.js
import styled from 'styled-components';
import Button from './Button';
const LargeButton = styled(Button)`
font-size: 20px;
padding: 12px 24px;
`;
const IconButton = styled(Button)`
display: flex;
align-items: center;
gap: 8px;
svg {
height: 16px;
width: 16px;
}
`;
export { LargeButton, IconButton };
Real-World Example: Creating a Card Component
Let's create a card component that can be used for displaying article previews:
// components/ArticleCard.js
import styled from 'styled-components';
import Link from 'next/link';
import Image from 'next/image';
const Card = styled.div`
display: flex;
flex-direction: column;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease, box-shadow 0.2s ease;
background-color: white;
&:hover {
transform: translateY(-4px);
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
}
`;
const ImageContainer = styled.div`
position: relative;
height: 200px;
width: 100%;
`;
const Content = styled.div`
padding: 16px;
`;
const Title = styled.h3`
margin: 0 0 8px 0;
font-size: 18px;
color: #333;
`;
const Description = styled.p`
margin: 0 0 16px 0;
font-size: 14px;
color: #666;
`;
const Tag = styled.span`
display: inline-block;
background-color: #f0f0f0;
color: #666;
font-size: 12px;
padding: 4px 8px;
border-radius: 4px;
margin-right: 8px;
`;
function ArticleCard({ article }) {
return (
<Card>
<ImageContainer>
<Image
src={article.coverImage}
alt={article.title}
layout="fill"
objectFit="cover"
/>
</ImageContainer>
<Content>
<Title>{article.title}</Title>
<Description>{article.description}</Description>
<div>
{article.tags.map(tag => (
<Tag key={tag}>{tag}</Tag>
))}
</div>
</Content>
</Card>
);
}
export default ArticleCard;
Using the ArticleCard component:
// pages/blog.js
import styled from 'styled-components';
import ArticleCard from '../components/ArticleCard';
const Grid = styled.div`
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 24px;
padding: 24px;
`;
export default function Blog() {
const articles = [
{
id: 1,
title: 'Getting Started with Next.js',
description: 'Learn the basics of Next.js and how to build your first app.',
coverImage: '/images/nextjs-basics.jpg',
tags: ['Next.js', 'React', 'Tutorial'],
},
{
id: 2,
title: 'CSS-in-JS Explained',
description: 'A deep dive into styling your React components with CSS-in-JS libraries.',
coverImage: '/images/css-in-js.jpg',
tags: ['CSS-in-JS', 'Styling', 'React'],
},
// More articles...
];
return (
<div>
<h1>Blog</h1>
<Grid>
{articles.map(article => (
<ArticleCard key={article.id} article={article} />
))}
</Grid>
</div>
);
}
Performance Considerations
While CSS-in-JS offers many benefits, there are some performance considerations to keep in mind:
- Runtime overhead: CSS-in-JS libraries add some JavaScript overhead at runtime
- Bundle size: Including a CSS-in-JS library increases your bundle size
- Server-side rendering complexity: Without proper setup, CSS-in-JS can lead to the "flash of unstyled content"
To optimize performance:
- Use a CSS-in-JS library that supports static extraction when possible
- Consider using
@emotion/css
instead of@emotion/styled
for smaller bundle sizes - Ensure proper server-side rendering setup
- For highly performance-critical applications, consider CSS Modules as an alternative
Summary
CSS-in-JS in Next.js provides a powerful way to style components with the full power of JavaScript. We've covered:
- Setting up popular CSS-in-JS libraries like styled-components and Emotion
- Creating styled components with dynamic props
- Using advanced features like theming and global styles
- Building real-world components with CSS-in-JS
- Understanding performance considerations
Both styled-components and Emotion offer similar APIs and features, so the choice between them often comes down to personal preference and specific project needs. CSS-in-JS works especially well for component libraries and applications with dynamic, prop-based styling needs.
Additional Resources
Exercises
- Create a responsive navigation bar using styled-components with mobile and desktop layouts
- Implement a dark/light theme toggle using CSS-in-JS with React context
- Build a form with styled components that validate input and show error states
- Create a card component that changes appearance based on a "featured" prop
- Try refactoring an existing component styled with CSS Modules to use CSS-in-JS
By mastering CSS-in-JS in Next.js, you'll have a powerful styling approach in your toolbox that integrates seamlessly with your component-based architecture.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)