Themes & Customization·6 min read·

Adding a dark mode toggle readers actually use

How to wire a dark/light toggle on a tenant blog using the existing next-themes context plus a simple sun/moon button — no manual cookie or CSS variable juggling.

Dark mode is the second-most asked-for feature on any blog after a working search bar. Readers expect it. The good news: every VeloCMS blog already runs through a `ThemeProvider` that handles system preference detection, persistence in localStorage, and the hydration handshake that prevents the white-flash on page load. You don't need to ship a separate context — you just need to surface a button.

Where the provider lives

Open `src/components/theme/theme-provider.tsx`. The provider wraps every page from the root layout and reads `defaultTheme="system"`. It writes a `data-theme` attribute on `<html>` (light/dark/system), and your theme.css uses that attribute via the `body[data-theme="light"] / [data-theme="dark"]` selectors that the port-theme-css script already injected for you.

Translation: the plumbing is done. You only need a control.

Drop a sun/moon button into your theme nav

Open your theme's home/blog/post/page layout (e.g., `AtelierBlogLayout.tsx`). Inside the `<nav className="site-nav">` block, add a single client component:

import { ThemeToggle } from "@/components/theme/theme-toggle";

// inside your nav-links group:
<ThemeToggle />

That's it. `ThemeToggle` is already shipped — it renders a sun or moon SVG depending on the resolved theme, animates the swap with Framer Motion, and writes back to the provider. It's a 60-line component, no dependencies you don't already have.

Optional: hide the button on the 'system' state

Some designers prefer to show a button only when the visitor has actively chosen a non-default mode. In `theme-toggle.tsx`, the `resolvedTheme` hook tells you what's actually applied. Conditionally render the button when `theme !== "system"` — visitors who haven't picked yet see nothing, and the experience stays minimal.

Verify with the keyboard

Press Tab through your blog header. The toggle must receive focus and respond to Space/Enter. The shipped component has `aria-label="Toggle dark mode"` and `role="button"` plus the proper focus ring — but if you've customised the nav with hand-rolled CSS, double-check the focus state isn't hidden by an `outline: none` rule that crept in via a theme reset.