Challenges to Overcome
While Sass is powerful for writing cleaner, more modular CSS, it has limitations for runtime theming, as Sass code is compiled before the browser renders it. This means that pure Sass can't dynamically switch themes at runtime. However, by combining CSS variables with Sass, we can achieve runtime theming with minimal extra effort.
Setting Up a Theme Context in Next.js
The context provider will manage the theme state across components. The example code here is written for a Next.js 14 project, but it's adaptable for any React setup.
// ThemeProvider.tsx
'use client'
import React, { createContext, FC, useCallback, useContext, useLayoutEffect, useMemo, useState } from 'react';
import './ThemeProvider.scss';
type Theme = 'dark' | 'light';
type ContextReturnType = {
theme: Theme;
toggleTheme: () => void;
};
type Props = {
children: React.ReactNode;
};
const ThemeContext = createContext<ContextReturnType>({
theme: 'dark',
toggleTheme: () => {},
});
const ThemeProvider: FC<Props> = ({ children }) => {
const [theme, setTheme] = useState<Theme>(() => {
if (typeof window !== 'undefined') {
const savedTheme = localStorage.getItem('theme');
return (savedTheme || 'dark') as Theme;
}
return 'dark';
});
const [isLoading, setIsLoading] = useState(true);
const toggleTheme = useCallback(() => {
setTheme(prevTheme => {
const nextTheme = prevTheme === 'light' ? 'dark' : 'light';
localStorage.setItem('theme', nextTheme);
return nextTheme;
});
}, []);
useLayoutEffect(() => {
document.documentElement.className = theme;
setIsLoading(false);
}, [theme]);
const contextValue = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme]);
if (isLoading) {
return null;
}
return <ThemeContext.Provider value={contextValue}>{children}</ThemeContext.Provider>;
};
export const useThemeContext = () => useContext(ThemeContext);
export default ThemeProvider;
The theme is stored in the state and saved to localStorage so the user's preference persists across sessions. The toggleTheme function switches between light and dark themes and updates localStorage. We use useLayoutEffect to apply the theme class to the HTML element, enabling us to style based on this class in Sass.
Defining the Theme with Sass
Next, let's define our theme styles using Sass. We'll create two mixins, Text and Background, to handle colors for text and background but you can have as many as you need.
// theme.scss
@mixin Text($primary, $secondary) {
--text-primary: #{$primary};
--text-secondary: #{$secondary};
}
@mixin Background($primary, $secondary) {
--background-primary: #{$primary};
--background-secondary: #{$secondary};
}
.dark {
--theme: dark;
@include Text('#F9F9F9', '#C6C6C6');
@include Background('#0D0E10', '#1E1F24');
}
.light {
--theme: light;
@include Text('#1C1C1C', '#4A4A4A');
@include Background('#FFFFFF', '#f2f7ff');
}
The Text and Background mixins allow us to easily apply color schemes for each theme. The .dark and .light classes define CSS variables for each theme. The --theme property lets us identify which theme is active within Sass styles. You can use that to apply theme-specific styles.
Using CSS Variables in Sass
To make our theme values available in Sass, let's create a helper function that converts CSS variables into Sass variables.
// mixins.scss
@function theme($color-name) {
@return var(--#{$color-name});
}
The theme function retrieves CSS variable values by their name, allowing you to use theme('text-primary') in any Sass file where you want to access theme-dependent styles.
Applying Themes to Components
Now, let's apply these theme variables to a component. Here's how to style a button component with theme-dependent colors.
// Button.module.scss
@import 'mixins.scss';
.button {
color: theme('text-primary');
background-color: theme('background-primary');
}
The Button component now uses theme-specific colors for its text and background, automatically adapting to the active theme.
Conclusion
Following these steps, you can easily implement dynamic themes in your Next.js project.
You can find more articles, written by me on my website.
Top comments (0)