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