One of the key aspects of building a polished and professional application is consistent theming. Theming allows you to define a visual identity for your application, controlling elements like colors, typography, spacing, and component styles. In Next.js, you have several powerful options for implementing theming, ranging from simple CSS variables to sophisticated libraries.
We'll explore common approaches to theming in Next.js, focusing on how to create a scalable and maintainable styling system.
CSS variables are a fundamental and highly effective way to manage themes. You can define your theme variables globally (e.g., in _app.js or a global CSS file) and then use them throughout your application. This makes it incredibly easy to change your theme by updating a few variables in one place.
/* styles/globals.css */
:root {
--primary-color: #0070f3;
--secondary-color: #18a0fb;
--background-color: #ffffff;
--text-color: #333333;
--font-family: 'Inter', sans-serif;
--spacing-unit: 8px;
}
[data-theme='dark'] {
--primary-color: #00c4b3;
--secondary-color: #2de6d4;
--background-color: #1e1e1e;
--text-color: #f0f0f0;
}
body {
background-color: var(--background-color);
color: var(--text-color);
font-family: var(--font-family);
}
h1 {
color: var(--primary-color);
}
button {
background-color: var(--primary-color);
color: white;
padding: var(--spacing-unit) calc(var(--spacing-unit) * 2);
border: none;
cursor: pointer;
}// pages/_app.js
import '../styles/globals.css';
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default MyApp;To implement theme switching with CSS variables, you can toggle a class or data attribute on the <body> or <html> element. This can be managed with React state and context.
import { useState, useEffect, createContext, useContext } from 'react';
const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light'); // 'light' or 'dark'
useEffect(() => {
document.body.setAttribute('data-theme', theme);
}, [theme]);
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => useContext(ThemeContext);// pages/_app.js (updated)
import '../styles/globals.css';
import { ThemeProvider } from '../context/ThemeContext'; // Assuming you save this in a context folder
function MyApp({ Component, pageProps }) {
return (
<ThemeProvider>
<Component {...pageProps} />
</ThemeProvider>
);
}
export default MyApp;// components/ThemeToggle.js
import { useTheme } from '../context/ThemeContext';
function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'Dark' : 'Light'} Mode
</button>
);
}
export default ThemeToggle;CSS-in-JS libraries like Styled Components and Emotion offer a more programmatic approach to styling and theming. They allow you to write CSS directly within your JavaScript components, making it easier to manage dynamic styles and theme integration.
Styled Components provides a ThemeProvider component that allows you to pass your theme object down the component tree. Components can then access these theme values using props.
// theme.js
export const lightTheme = {
colors: {
primary: '#0070f3',
secondary: '#18a0fb',
background: '#ffffff',
text: '#333333',
},
fonts: {
body: 'Inter, sans-serif',
},
};
export const darkTheme = {
colors: {
primary: '#00c4b3',
secondary: '#2de6d4',
background: '#1e1e1e',
text: '#f0f0f0',
},
fonts: {
body: 'Inter, sans-serif',
},
};// pages/_app.js
import { ThemeProvider } from 'styled-components';
import { lightTheme, darkTheme } from '../theme'; // Assuming theme.js is in a theme folder
import { useState } from 'react';
function MyApp({ Component, pageProps }) {
const [isDarkMode, setIsDarkMode] = useState(false);
const currentTheme = isDarkMode ? darkTheme : lightTheme;
const toggleTheme = () => {
setIsDarkMode(!isDarkMode);
};
return (
<ThemeProvider theme={currentTheme}>
<Component {...pageProps} />
<button onClick={toggleTheme}>
Switch to {isDarkMode ? 'Light' : 'Dark'} Mode
</button>
</ThemeProvider>
);
}
export default MyApp;import styled from 'styled-components';
const Button = styled.button`
background-color: ${props => props.theme.colors.primary};
color: white;
padding: 8px 16px;
border: none;
cursor: pointer;
font-family: ${props => props.theme.fonts.body};
`;
export default Button;Emotion also offers a ThemeProvider, similar to Styled Components. The integration and usage are conceptually alike.
// pages/_app.js (using Emotion)
import { ThemeProvider } from '@emotion/react';
import { lightTheme, darkTheme } from '../theme'; // Same theme objects
import { useState } from 'react';
function MyApp({ Component, pageProps }) {
const [isDarkMode, setIsDarkMode] = useState(false);
const currentTheme = isDarkMode ? darkTheme : lightTheme;
const toggleTheme = () => {
setIsDarkMode(!isDarkMode);
};
return (
<ThemeProvider theme={currentTheme}>
<Component {...pageProps} />
<button onClick={toggleTheme}>
Switch to {isDarkMode ? 'Light' : 'Dark'} Mode
</button>
</ThemeProvider>
);
}
export default MyApp;/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
const buttonStyle = (theme) => css`
background-color: ${theme.colors.primary};
color: white;
padding: 8px 16px;
border: none;
cursor: pointer;
font-family: ${theme.fonts.body};
`;
function MyButton() {
return <button css={buttonStyle}>Click Me</button>;
}
export default MyButton;If you're using Tailwind CSS, theming is typically managed through its configuration file (tailwind.config.js). You can define multiple color palettes, font families, and other design tokens. Tailwind's darkMode option allows for easy switching between themes (e.g., 'media' for system preference, or 'class' for manual control).
// tailwind.config.js
module.exports = {
darkMode: 'class', // or 'media'
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {
colors: {
primary: {
light: '#0070f3',
DEFAULT: '#0070f3',
dark: '#00c4b3',
},
secondary: '#18a0fb',
background: {
light: '#ffffff',
dark: '#1e1e1e',
},
text: {
light: '#333333',
dark: '#f0f0f0',
},
},
fontFamily: {
inter: ['Inter', 'sans-serif'],
},
},
},
plugins: [],
};// pages/_app.js (for class-based dark mode)
import '../styles/globals.css';
import { useState, useEffect } from 'react';
function MyApp({ Component, pageProps }) {
const [isDarkMode, setIsDarkMode] = useState(false);
useEffect(() => {
const htmlElement = document.documentElement;
if (isDarkMode) {
htmlElement.classList.add('dark');
} else {
htmlElement.classList.remove('dark');
}
}, [isDarkMode]);
const toggleTheme = () => {
setIsDarkMode(!isDarkMode);
};
return (
<div>
<Component {...pageProps} />
<button onClick={toggleTheme} className="bg-primary-light text-white p-2 rounded dark:bg-primary-dark">
Switch to {isDarkMode ? 'Light' : 'Dark'} Mode
</button>
</div>
);
}
export default MyApp;With darkMode: 'class', you add the dark class to the <html> or <body> element and then use Tailwind's dark: variants for styles that should apply in dark mode. For example, dark:bg-gray-800 will apply a dark gray background only when the dark class is present.
The best theming strategy depends on your project's complexity and your team's preferences:
- CSS Variables: Excellent for simple to moderately complex themes, especially if you prefer standard CSS or want maximum performance. Easy to implement and understand.
- CSS-in-JS: Ideal for highly dynamic themes, component-level styling, and when you want to leverage JavaScript logic for styling. Offers strong encapsulation.
- Tailwind CSS: Perfect if you're already using Tailwind and appreciate its utility-first approach. Provides a robust configuration for defining and switching themes.
Regardless of the chosen method, consistency is key. Define your theme tokens clearly and use them throughout your application to ensure a cohesive user experience.