Skip to main content

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:

  1. Automatically detects user preferences for light or dark mode
  2. Offers a simple way to toggle between themes
  3. Remembers theme choices across page refreshes
  4. 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 the dark 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 file
  • hsl() is a color function representing Hue, Saturation, and Lightness
  • var() 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 theming
  • defaultTheme="system" - Default to the user's system preference
  • enableSystem - 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:

  1. When a user clicks the theme toggle button
  2. The button calls setTheme() with the new theme value
  3. ThemeProvider updates the HTML element by adding or removing the "dark" class
  4. The theme preference is saved to localStorage for persistence
  5. When the HTML class changes, different CSS variables become active
  6. Components using these CSS variables instantly update their appearance

Inside next-themes

The next-themes library handles the complex parts:

  1. 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)
  2. Theme Persistence: When a user selects a theme, it's saved to localStorage so it remains the same when they return.

  3. 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