Skip to main content

Theme Hooks

import { useThemePreference } from "@noxion/renderer";

@noxion/renderer provides the useThemePreference() hook for managing color mode (light/dark/system). Theme components, layouts, and templates are imported directly from theme packages — no provider required.


Setup

// app/layout.tsx
import "@noxion/theme-default/styles/tailwind";
import "./globals.css";

function ThemeScript() {
const script = `
(function() {
try {
var stored = localStorage.getItem('noxion-theme');
var theme = stored || 'system';
if (theme === 'system') {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
document.documentElement.dataset.theme = theme;
} catch (e) {}
})();
`;
return <script dangerouslySetInnerHTML={{ __html: script }} />;
}

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<ThemeScript />
</head>
<body>{children}</body>
</html>
);
}
// app/site-layout.tsx
"use client";
import { BlogLayout, Header, Footer } from "@noxion/theme-default";

export function SiteLayout({ children }: { children: React.ReactNode }) {
return (
<BlogLayout
slots={{
header: () => <Header siteName="My Blog" navigation={[{ label: "Home", href: "/" }]} />,
footer: () => <Footer siteName="My Blog" author="Author" />,
}}
>
{children}
</BlogLayout>
);
}

Why suppressHydrationWarning?

The <ThemeScript> sets data-theme="light" or data-theme="dark" on <html> before React hydrates. This creates a mismatch between the server-rendered HTML (no data-theme attribute) and the client-side DOM (with data-theme). React would normally warn about this hydration mismatch, so suppressHydrationWarning silences it.

This is a well-known pattern for theme systems. See React docs on suppressHydrationWarning.


useThemePreference()

Returns the user's preference setting (including "system"), the resolved mode, and a function to change it.

Signature

function useThemePreference(): {
preference: ThemePreference; // "light" | "dark" | "system"
resolved: "light" | "dark"; // actual applied mode
setPreference: (pref: ThemePreference) => void;
}

Returns

PropertyTypeDescription
preferenceThemePreferenceThe user's stored preference. "system" if following OS setting.
resolved"light" | "dark"The actual applied mode after resolving "system" against the OS preference.
setPreference(pref: ThemePreference) => voidUpdate the preference. Persisted to localStorage and immediately applied.

Usage

"use client";
import { useThemePreference } from "@noxion/renderer";
import type { ThemePreference } from "@noxion/renderer";

function ThemeToggle() {
const { preference, setPreference } = useThemePreference();

const cycleTheme = () => {
const order: ThemePreference[] = ["system", "light", "dark"];
const currentIndex = order.indexOf(preference);
setPreference(order[(currentIndex + 1) % order.length]);
};

return (
<button onClick={cycleTheme} aria-label={`Current theme: ${preference}`}>
{preference === "light" ? "☀️" : preference === "dark" ? "🌙" : "💻"}
</button>
);
}

Persistence

setPreference() writes the preference to localStorage under the key "noxion-theme". On subsequent page loads, <ThemeScript> reads this key and applies the preference before React hydrates.


Theme resolution logic

The full theme resolution flow:

1. User visits page
2. <ThemeScript> runs synchronously in <head>:
a. Checks localStorage["noxion-theme"]
b. If set: use that value ("light" or "dark")
c. If "system" or not set: check window.matchMedia("(prefers-color-scheme: dark)")
d. Set <html data-theme="light|dark">
3. React hydrates — useThemePreference() reads data-theme from <html>
4. User clicks toggle → setPreference() updates localStorage AND data-theme
5. CSS responds to [data-theme="dark"] selector
6. Tailwind dark: utilities activate via @custom-variant

This architecture ensures zero FOUC (Flash of Unstyled Content) regardless of the user's preference or network speed.

Media query responsiveness

When preference === "system", the theme responds to real-time OS preference changes:

// Inside useThemePreference()
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
mediaQuery.addEventListener("change", (e) => {
if (preference === "system") {
applyTheme(e.matches ? "dark" : "light");
}
});

This means if the user switches their OS from light to dark mode while on your blog (with preference === "system"), the blog updates automatically without a page reload.