Themes
Journey con Themes
Photo by Jose G. Ortega Castro on Unsplash I've always, i mean always been huge fan of different themes. Never been a day i've not switched my terminal / neovim theme. And when came to web I used to think it was some complex magic happening behind the scenes, but when I discovered how straightforward it is with Tailwind CSS, I was amazed! I started with implementing a simple light and dark theme switch in my applications.
When, I stumbled upon monkeytype.com Bottom right to change themes - a beautifully minimalistic typing website. What caught my attention wasn't just its clean interface and vast customizations, but its incredible theme customization options. As someone who's always been a huge fan of the Gruvbox (I use it everywhere - my terminal, Neovim setup, you name it!), I was inspired to create something similar.
I initially thought about packaging this component and publishing it for others to use. While my first attempt at publishing the package didn't quite work out as planned (installation issues, you know how it goes 😅), So i took down the published package and I ended up with a solid implementation that I now use across my applications, including vimtrim.xyz and various other projects.
Two Approaches: Next.js and React
I've implemented theme switching in both Next.js and React applications. Let's explore both approaches!
1. Next.js Implementation with next-themes
The Next.js implementation uses the next-themes
package for seamless theme switching:
interface ThemeToggleProps {
mode?: 'toggle' | 'select';
}
const ThemeToggle: React.FC<ThemeToggleProps> = ({ mode = "toggle" }) => {
const { theme, setTheme } = useTheme();
const [isRotating, setIsRotating] = useState(false);
// Toggle mode implementation
if (mode === "toggle") {
const toggleTheme = () => {
setIsRotating(true);
setTimeout(() => {
setTheme(theme === "dark" ? "light" : "dark");
setIsRotating(false);
}, 150);
};
return (
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
aria-label="Toggle theme"
>
<Sun className={cn("h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all",
{ "rotate-90 scale-0": theme === "dark" })}
/>
<Moon className={cn("absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all",
{ "rotate-0 scale-100": theme === "dark" })}
/>
</Button>
);
}
// Select mode implementation
return (
<Select onValueChange={setTheme} value={theme}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select a theme" />
</SelectTrigger>
<SelectContent aria-label="Theme selector">
{themeNames.map((themeName) => (
<SelectItem key={themeName} value={themeName}>
{themeLabels[themeName]}
</SelectItem>
))}
</SelectContent>
</Select>
);
};
2. React Implementation with Context API
For React applications, we can use the Context API for theme management.
First, we need to set up our AppContext in our root App.tsx:
import react, { createcontext, usestate, usecallback } from "react";
import { themename, themes } from "./constants";
import themeswitcher from "./components/themeswitcher.tsx";
import { button } from "./components/ui/button.tsx";
type appcontexttype = {
theme: themename;
settheme: (theme: themename) => void;
};
export const appcontext = createcontext<appcontexttype>({} as appcontexttype);
const app = () => {
const [theme, setthemestate] = usestate<themename>(themes.gruvbox_dark);
const settheme = usecallback((newtheme: themename) => {
setthemestate(newtheme);
}, []);
const contextvalue: appcontexttype = {
theme,
settheme,
};
return (
<appcontext.provider value={contextvalue}>
<themeswitcher />
<button>hello</button>
</appcontext.provider>
);
};
export default app;
Then, we can create our theme switcher component. Here's the implementation with shadcn/ui Select ( just to make life easier ) :
import { useContext, useEffect } from "react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { AppContext } from "../App";
import { ThemeName, themeNames, themeLabels } from "../constants/themes";
const ThemeSwitcher = () => {
const { theme, setTheme } = useContext(AppContext);
useEffect(() => {
document.body.className = theme;
}, [theme]);
const handleThemeChange = (newTheme: ThemeName) => {
setTheme(newTheme);
};
return (
<Select onValueChange={handleThemeChange} value={theme}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select a theme" />
</SelectTrigger>
<SelectContent aria-label="color theme selector">
{themeNames.map((themeName) => (
<SelectItem key={themeName} value={themeName}>
{themeLabels[themeName]}
</SelectItem>
))}
</SelectContent>
</Select>
);
};
export default ThemeSwitcher;
Alternatively, you can implement the theme switcher with a basic HTML select if you prefer not to use shadcn/ui:
const BasicThemeSwitcher = () => {
const { theme, setTheme } = useContext(AppContext);
useEffect(() => {
document.body.className = theme;
}, [theme]);
return (
<select
value={theme}
onChange={(e) => setTheme(e.target.value as ThemeName)}
className="w-[180px] p-2 rounded border bg-background text-foreground"
>
{themeNames.map((themeName) => (
<option key={themeName} value={themeName}>
{themeLabels[themeName]}
</option>
))}
</select>
);
};
Shared Types and Constants
Both implementations share common type definitions and constants:
export type ThemeName = "light" | "dark" | "gruvbox" | "midnight";
export const themeNames: ThemeName[] = ["light", "dark", "gruvbox", "midnight"];
export const themeLabels: Record<ThemeName, string> = {
light: "Light",
dark: "Dark",
gruvbox: "Gruvbox",
midnight: "Midnight",
};
CSS Implementation
The CSS implementation remains consistent across both approaches:
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
/* Additional theme variables */
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
/* Additional theme variables */
}
.gruvbox {
--background: 50 10% 16%;
--foreground: 50 10% 98%;
/* Additional theme variables */
}
.midnight {
--background: 217 32% 17%;
--foreground: 217 32% 98%;
/* Additional theme variables */
}
Key Differences Between Implementations
-
State Management:
- Next.js: Uses
next-themes
for persistent theme storage and SSR compatibility - React: Uses Context API with manual class application to
document.body
- Next.js: Uses
-
Theme Persistence:
- Next.js: Handled automatically by
next-themes
- React: Needs additional implementation (e.g., localStorage) for persistence
- Next.js: Handled automatically by
Conclusion
Have fun
You can find the complete code for react implementation on GitHub: Demo setup with react
Gracias.