Themes

Journey con Themes

Catazajá, Chis., México

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

  1. 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
  2. Theme Persistence:

    • Next.js: Handled automatically by next-themes
    • React: Needs additional implementation (e.g., localStorage) for persistence

Conclusion

Have fun

You can find the complete code for react implementation on GitHub: Demo setup with react

Gracias.