40+ landing page components for ReactBrowse now

Guides

Dark Mode

Learn how trink-ui handles dark mode through CSS variables and the .dark class strategy. Implement a toggle, detect system preferences, and use per-section theming.

How CSS variable theming works

trink-ui defines two sets of CSS variables: one under :root for light mode and one under .dark for dark mode. When the dark class is present on the <html> element, all CSS variables switch to their dark values, and every component automatically updates.

globals.css
:root {
  --trinkui-bg: 255 255 255;        /* white */
  --trinkui-fg: 10 10 10;           /* near-black */
  --trinkui-primary: 99 102 241;    /* indigo-500 */
  --trinkui-surface: 248 250 252;   /* light gray */
}

.dark {
  --trinkui-bg: 9 9 11;             /* near-black */
  --trinkui-fg: 250 250 250;        /* near-white */
  --trinkui-primary: 129 140 248;   /* indigo-400 */
  --trinkui-surface: 18 18 21;      /* dark gray */
}

Adding the dark class

The simplest way to enable dark mode is to add the dark class to your HTML element. In Next.js App Router:

app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className="dark">
      <body>{children}</body>
    </html>
  );
}

For a toggle-based approach, you will add and remove this class dynamically with JavaScript.

Toggle implementation with localStorage

Here is a complete dark mode toggle component that persists the user's preference in localStorage:

components/ThemeToggle.tsx
"use client";

import { useEffect, useState } from "react";

export function ThemeToggle() {
  const [isDark, setIsDark] = useState(false);

  useEffect(() => {
    // Check localStorage on mount
    const stored = localStorage.getItem("theme");
    if (stored === "dark") {
      setIsDark(true);
      document.documentElement.classList.add("dark");
    } else if (stored === "light") {
      setIsDark(false);
      document.documentElement.classList.remove("dark");
    }
  }, []);

  function toggle() {
    const next = !isDark;
    setIsDark(next);
    if (next) {
      document.documentElement.classList.add("dark");
      localStorage.setItem("theme", "dark");
    } else {
      document.documentElement.classList.remove("dark");
      localStorage.setItem("theme", "light");
    }
  }

  return (
    <button
      onClick={toggle}
      aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
      className="rounded-lg border border-[rgb(var(--trinkui-border))] p-2 text-[rgb(var(--trinkui-muted))] hover:bg-[rgb(var(--trinkui-surface))] hover:text-[rgb(var(--trinkui-fg))] transition-colors"
    >
      {isDark ? (
        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
          <circle cx="12" cy="12" r="5" />
          <path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
        </svg>
      ) : (
        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
          <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
        </svg>
      )}
    </button>
  );
}

Detecting system preference

To respect the user's operating system preference as a default, check prefers-color-scheme when no stored preference exists:

components/ThemeProvider.tsx
"use client";

import { useEffect } from "react";

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    const stored = localStorage.getItem("theme");

    if (stored === "dark") {
      document.documentElement.classList.add("dark");
    } else if (stored === "light") {
      document.documentElement.classList.remove("dark");
    } else {
      // No stored preference — use system preference
      const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
      if (prefersDark) {
        document.documentElement.classList.add("dark");
      }
    }

    // Listen for system preference changes
    const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
    function handleChange(e: MediaQueryListEvent) {
      if (!localStorage.getItem("theme")) {
        if (e.matches) {
          document.documentElement.classList.add("dark");
        } else {
          document.documentElement.classList.remove("dark");
        }
      }
    }

    mediaQuery.addEventListener("change", handleChange);
    return () => mediaQuery.removeEventListener("change", handleChange);
  }, []);

  return <>{children}</>;
}

Wrap your root layout with this provider to automatically apply the correct theme on page load.

Per-section theming

trink-ui section components accept a theme prop that overrides the page-level theme for that section only. This lets you create visual contrast by alternating light and dark sections on the same page:

{/* Light page with dark accent sections */}
<HeroCentered theme="light" title="Light hero" ... />
<FeaturesGrid theme="light" title="Light features" ... />
<TestimonialsGrid theme="dark" title="Dark testimonials" ... />
<FAQAccordion theme="light" title="Light FAQ" ... />
<CTACentered theme="dark" title="Dark CTA" ... />
<FooterColumns theme="dark" ... />

The theme prop works independently of the page-level dark class. A section with theme="dark" will always render with dark colors, even on a light-mode page.

Custom dark mode colors

Override any dark mode variable to create a custom dark palette:

globals.css — Custom dark theme
.dark {
  /* Deep navy background instead of near-black */
  --trinkui-bg: 15 23 42;
  --trinkui-fg: 241 245 249;
  --trinkui-surface: 30 41 59;
  --trinkui-border: 51 65 85;
  --trinkui-muted: 148 163 184;

  /* Teal primary instead of indigo */
  --trinkui-primary: 45 212 191;
  --trinkui-primary-fg: 15 23 42;
}

Preventing flash of wrong theme

To avoid a flash of the wrong theme on page load, add an inline script to the <head> that runs before React hydrates:

app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <script
          dangerouslySetInnerHTML={{
            __html: `
              (function() {
                var theme = localStorage.getItem('theme');
                if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
                  document.documentElement.classList.add('dark');
                }
              })();
            `,
          }}
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

Next step

Learn how to master animations with FadeIn, SlideUp, and StaggerChildren.

Animations Guide