Chapter 3: Theme System
In the previous chapter, we explored how ShipNowKit's Database Services act as "data librarians" for your application. Now, let's turn our attention to making your application visually appealing in any lighting condition with the Theme System.
Introduction
Have you ever used an app late at night and been blinded by its bright white interface? Or struggled to read dark text on a dark background? That's what the Theme System solves!
Think of the Theme System like a light switch for your entire application. With one toggle, users can switch between light mode (bright background, dark text) and dark mode (dark background, light text) based on their preference or environment.
The Problem: Inconsistent Visual Experience
Without a proper theme system, implementing light and dark modes becomes challenging:
- You would need to manually set colors for each component
- Maintaining consistent styling across the application is difficult
- Responding to user preferences requires custom code throughout
- Switching themes could cause jarring visual changes
ShipNowKit's Theme System Solution
ShipNowKit's Theme System provides a complete solution that:
- Automatically detects user preferences for light or dark mode
- Offers a simple way to toggle between themes
- Remembers theme choices across page refreshes
- Ensures all UI components respond appropriately to theme changes
Let's see how to implement it in your application!
Key Components of the Theme System
1. ThemeProvider
The foundation of the Theme System is the ThemeProvider
component:
// providers/ThemeProvider.tsx
"use client";
import {
ThemeProvider as NextThemesProvider,
type ThemeProviderProps,
} from "next-themes";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
This is a wrapper around the next-themes
library, which handles theme detection and switching. The "use client"
directive indicates this is a client-side component since theme switching happens in the browser.
2. Theme Toggle Button
Users need a way to switch between themes. Here's a simple toggle button:
// components/ThemedButton.tsx
"use client";
import { Moon } from "@/components/icons/Moon";
import { Sun } from "@/components/icons/Sun";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
export function ThemedButton() {
const [mounted, setMounted] = useState(false);
const { theme, setTheme } = useTheme();
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return <Sun />;
}
return (
<div onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
{theme === "light" ? <Moon /> : <Sun />}
</div>
);
}
This component:
- Uses
useTheme()
hook to access the current theme and change it - Shows a moon icon in light mode (click to switch to dark)
- Shows a sun icon in dark mode (click to switch to light)
- Has a "mounted" check to prevent hydration errors (differences between server and client rendering)
3. Theme-Aware Colors in Tailwind
ShipNowKit uses Tailwind CSS to make components automatically adapt to theme changes:
// tailwind.config.ts (simplified)
const config = {
darkMode: ["class"],
theme: {
extend: {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
// More colors...
}
}
}
}
The key parts are:
darkMode: ["class"]
- This tells Tailwind to apply dark styles when thedark
class is on the HTML element- Color definitions that use CSS variables like
var(--background)
- These change values between light and dark modes
4. CSS Variables for Theme-Aware Styling
In the Tailwind CSS configuration file, background: hsl(var(--background))
defines a color variable named "background" that gets its value from the CSS variable --background
.
This approach combines CSS variables (Custom Properties) with the HSL color format:
--background
is a CSS variable, typically defined in your global CSS filehsl()
is a color function representing Hue, Saturation, and Lightnessvar()
is a CSS function used to reference the value of a CSS variable
This design pattern is particularly well-suited for implementing theme switching (like dark mode and light mode). By changing the values of CSS variables under different theme classes, you can easily switch the entire application's color scheme without needing to modify numerous CSS rules.
For example, your CSS file might have definitions like this:
:root {
--background: 0 0% 100%; /* White, for light mode */
--foreground: 222.2 84% 4.9%; /* Dark, for text in
light mode */
}
.dark {
--background: 222.2 84% 4.9%; /* Dark, for dark mode
*/
--foreground: 0 0% 100%; /* White, for text in dark
mode */
}
When using Tailwind classes like bg-background
or text-foreground
, the colors will automatically change based on the current theme. This is why all components using these classes immediately respond to theme changes without requiring additional JavaScript code.
Setting Up the Theme System
Let's walk through how to set up the Theme System in your application:
Step 1: Wrap Your App with ThemeProvider
First, you need to wrap your application with the ThemeProvider:
// app/layout.tsx
import { ThemeProvider } from "@/providers/ThemeProvider";
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
>
{children}
</ThemeProvider>
</body>
</html>
);
}
The ThemeProvider accepts important props:
attribute="class"
- Use CSS classes for themingdefaultTheme="system"
- Default to the user's system preferenceenableSystem
- Enable detection of system preferences
Step 2: Add the Theme Toggle to Your UI
Now, add the theme toggle button to your navigation or header:
// components/Header.tsx
import { ThemedButton } from "@/components/ThemedButton";
export function Header() {
return (
<header className="flex justify-between p-4">
<h1 className="text-xl font-bold">My ShipNowKit App</h1>
<div className="flex items-center gap-4">
<ThemedButton />
{/* Other header elements */}
</div>
</header>
);
}
This places the theme toggle button in your header for easy access.
Step 3: Use Theme-Aware Classes in Your Components
When building components, use Tailwind's theme-aware classes:
// components/Card.tsx
export function Card({ title, content }) {
return (
<div className="p-6 rounded-lg bg-card text-card-foreground shadow">
<h3 className="text-lg font-medium">{title}</h3>
<p className="mt-2 text-muted-foreground">{content}</p>
</div>
);
}
Classes like bg-card
, text-card-foreground
, and text-muted-foreground
will automatically update when the theme changes!
How the Theme System Works Under the Hood
When a user interacts with your application using the Theme System, here's what happens:
- When a user clicks the theme toggle button
- The button calls
setTheme()
with the new theme value - ThemeProvider updates the HTML element by adding or removing the "dark" class
- The theme preference is saved to localStorage for persistence
- When the HTML class changes, different CSS variables become active
- Components using these CSS variables instantly update their appearance
Inside next-themes
The next-themes
library handles the complex parts:
-
Theme Detection: It checks for existing preferences in this order:
- Previously saved theme in localStorage
- User's system preference (light/dark)
- Falls back to the default theme (usually light)
-
Theme Persistence: When a user selects a theme, it's saved to localStorage so it remains the same when they return.
-
System Preference Sync: If set to "system" theme, it will update automatically when the user changes their device settings.
Using the Theme System in Your Projects
Let's look at some practical examples of how to use the Theme System:
Creating a Theme-Aware Button
// components/Button.tsx
export function Button({ children, ...props }) {
return (
<button
className="px-4 py-2 rounded bg-primary text-primary-foreground
hover:bg-primary/90 transition-colors"
{...props}
>
{children}
</button>
);
}
This button uses bg-primary
and text-primary-foreground
, which automatically update when the theme changes. No additional code needed!
Theme-Aware Icons
You can create icons that change based on the current theme:
// components/AlertIcon.tsx
"use client";
import { useTheme } from "next-themes";
export function AlertIcon() {
const { theme } = useTheme();
return (
<svg width="24" height="24" viewBox="0 0 24 24">
{/* Use theme to determine icon color */}
<path
fill={theme === "dark" ? "#ffffff" : "#000000"}
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"
/>
</svg>
);
}
This icon directly uses the theme value to change its appearance.
Conclusion
ShipNowKit's Theme System provides an elegant solution for implementing light and dark modes in your application. By using ThemeProvider, theme-aware components, and CSS variables, you can create interfaces that look great in any lighting condition and adapt to user preferences.
Key takeaways:
- The Theme System uses
next-themes
to detect and manage theme preferences - A simple toggle button lets users switch between light and dark modes
- Theme preferences are saved between sessions
- Components using theme-aware classes automatically update when themes change