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)