Proper UI

Dark mode

Easily swap between light and dark modes with CSS variables.

Dark mode support

Dark mode in Proper UI is class-based, not media-query based. globals.css declares a custom variant:

@custom-variant dark (&:where(.dark-mode, .dark-mode *));

so adding .dark-mode anywhere in the ancestor chain flips every semantic token defined in theme.css's .dark-mode { … } block. A component written with semantic tokens — bg-primary, text-primary, border-secondary — is automatically correct in dark mode and needs zero dark: utilities of its own.

Reach for an explicit dark: utility only for the rare case a token can't express — swapping an image asset, inverting a logo mark, or a gradient stop with no semantic equivalent — and leave a comment explaining why when you do.

How to switch to dark mode

Whole-app toggle

Wrap your app in the ThemeProvider, which wraps next-themes and maps its light/dark values onto the .light-mode/.dark-mode classes:

// app/layout.tsx
import { ThemeProvider } from "@properui/ui/providers";

export default function RootLayout({ children }: { children: React.ReactNode }) {
    return (
        <html lang="en" suppressHydrationWarning>
            <body>
                <ThemeProvider>{children}</ThemeProvider>
            </body>
        </html>
    );
}

ThemeProvider exposes three states to your users — Light, Dark, and System — and injects a blocking script so there's no flash of the wrong theme on first paint. Read or change the current theme anywhere with the re-exported hook:

import { useTheme } from "@properui/ui/providers";

const { theme, resolvedTheme, setTheme } = useTheme();

A minimal toggle button:

"use client";
import { Moon01, Sun } from "@properui/icons";
import { ButtonUtility } from "@properui/ui/components/base/buttons/button-utility";
import { useTheme } from "@properui/ui/providers";

export const ThemeToggle = () => {
    const { resolvedTheme, setTheme } = useTheme();
    return (
        <ButtonUtility
            aria-label="Toggle dark mode"
            icon={resolvedTheme === "dark" ? Sun : Moon01}
            onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
        />
    );
};

If you'd rather manage the class yourself — no next-themes — toggle .dark-mode on the root element directly and persist the choice:

const isDark = document.documentElement.classList.toggle("dark-mode");
localStorage.setItem("theme", isDark ? "dark" : "light");

Section-scoped dark mode

Some sections should render permanently dark regardless of the page's theme — a dark footer, a dark call-to-action band. Put .dark-mode directly on that element instead of hardcoding colors:

<section className="dark-mode bg-primary text-primary">
    <Footer />
</section>

Every component inside resolves its dark tokens without any prop drilling, because the dark custom variant matches .dark-mode at any depth in the ancestor chain — not just on <html>.

FAQs