Unbranded harness. Light/dark mode. Cross-device persistence. Your brand, not ours.
The MVPs ship with a theme harness — not a branded template. Out of the box, everything is clean and unbranded. You apply your colors, your fonts, your identity. The harness handles the infrastructure: light/dark mode, system preference detection, localStorage persistence, and Redis sync for cross-device consistency.
The ThemeProvider wraps your app in providers.tsx. It detects the user's system preference (prefers-color-scheme) on first visit and applies it immediately — no flash of wrong theme.
When the user toggles themes (via the built-in theme switcher in the nav), the preference is saved to localStorage for instant next-visit loading AND to Redis keyed to their user ID.
On a different device, when the user logs in, the theme preference is read from Redis and applied. Their laptop and phone stay in sync without any custom code.
Tailwind CSS dark mode classes are used throughout. Your custom components just use dark: variants and they work with the theme system automatically.
Theme flicker — that flash of white before dark mode loads — is one of the most common complaints about Next.js apps. It happens because the theme preference is read from localStorage after hydration. The MVP solves this with a blocking script in the document head that reads the preference before the first paint. No flicker. Ever. This is the kind of detail that takes hours to get right and that every app needs.
Most theme systems save to localStorage and call it done. But when your user switches from their laptop to their phone, the theme resets. The MVP writes theme preference to Redis (keyed by user ID) on every toggle. When the user logs in on a new device, the theme is fetched from Redis before the first render. localStorage is the fast cache, Redis is the source of truth.
// This happens automatically in ThemeProvider
// You never write this code
// On theme toggle:
localStorage.setItem('theme', 'dark');
await fetch('/api/user/preferences', {
method: 'PATCH',
body: JSON.stringify({ theme: 'dark' })
});
// ^ writes to Redis: user:42:prefs -> { theme: 'dark' }
// On login (new device):
const prefs = await fetch('/api/user/preferences');
// ^ reads from Redis, applies before renderThe theme system respects the user's OS-level preference by default. If they haven't explicitly chosen a theme, it follows prefers-color-scheme. Three states: "light", "dark", "system". The theme switcher shows all three options. "System" means: follow the OS, update in real-time if the OS switches (e.g., macOS auto dark mode at sunset).
The MVPs come out of the box completely unbranded. No PayEz logos, no platform colors, no "powered by" badges in your app. The theme harness is a neutral shell — clean backgrounds, neutral typography, sensible defaults. Every built-in screen runs through the harness: login, signup, 2FA challenge, admin dashboard, user profile, session management, role assignment — all of it. When you set your brand variables, every page in the entire application transforms. The auth screens your users see look like your product, not ours. The admin panel your team uses matches your identity. You bring your brand: your colors as CSS custom properties, your font stack, your logo. The harness applies them consistently across every page, component, and state (light, dark, loading, error). This is not a template you strip branding from — it's a harness that was never branded to begin with.
/* globals.css — define your brand */
:root {
--brand-primary: #your-color;
--brand-secondary: #your-color;
--brand-accent: #your-color;
--font-heading: 'Your Font', sans-serif;
--font-body: 'Your Font', sans-serif;
}
/* The harness reads these variables.
Every component, every page, every state
uses your brand — not ours.
No "powered by PayEz" anywhere.
No platform colors to override.
It's your app. */Every screen that ships with the MVP renders through the theme harness. Set your brand once — it applies everywhere: - Login screen — email/password form, social login buttons, "forgot password" link - Signup screen — registration form, plan selection, terms acceptance - 2FA challenge screen — method selection, code entry, masked contact display - Admin dashboard — all tabs: Users, Sessions, Audit, Logs, Roles - User profile / settings — avatar, preferences, 2FA enrollment - Error pages — 403, 404, 500 states - Loading states — skeleton screens, spinners - Upgrade prompts — tier gate modals, limit-reached notices None of these carry PayEz branding. They're neutral shells waiting for your CSS custom properties. Your users never see a platform brand — they see yours.
Tailwind's dark: variant works automatically. The theme system sets the appropriate class on the HTML element, and Tailwind handles the rest. Your custom components don't need to know about the theme system — they just use dark: classes.
// Your component — just use dark: variants
<div className="bg-white dark:bg-gray-900">
<h1 className="text-gray-900 dark:text-white">
Dashboard
</h1>
<p className="text-gray-600 dark:text-gray-400">
This adapts automatically.
</p>
</div>
// The ThemeProvider handles everything else.
// No useTheme hook needed in most components.Theme System ships with every @payez/next-mvp install. 10K MAU free. No credit card required.