How dark mode works on a VeloCMS blog (and how to add a reader toggle)
There's no shipped reader-facing toggle button today — dark/light on a tenant blog is a per-blog owner setting. Here's how the real system works and how to build your own toggle with next-themes.
Dark mode is the second-most asked-for feature on any blog after a working search bar, so it's worth understanding how VeloCMS actually handles it before you go build a toggle. Two separate mechanisms are involved: the global `ThemeProvider` (built on next-themes) that runs everywhere, and a per-blog `default_theme_mode` setting that decides whether a tenant's public blog even lets the reader's preference through.
Where the provider lives
Open `src/components/theme/theme-provider.tsx`. It wraps `next-themes` with `attribute="class"`, which toggles a `.dark` class on `<html>` — not a `data-theme` attribute. (`data-theme` in VeloCMS is reserved for a different concept: which theme PRESET is active, e.g. `data-theme="atelier"` — separate from light/dark mode entirely.) The provider persists the choice to a cookie via a server action and handles the hydration handshake that prevents a white-flash on load.
Why your blog might already be locked to one mode
Public tenant blogs don't automatically inherit the reader's system preference. A blog owner sets `default_theme_mode` (Admin → Settings) to one of: `preset` (follow whatever the active theme preset is designed for), `system` (defer to the reader's OS/browser preference), `light` (hard-locked), or `dark` (hard-locked). Only in the `system` case does a reader's own dark-mode preference actually apply — and even then, there's no visible toggle button shipped for public blog chrome today. Check your blog's setting first; building a toggle on a hard-locked blog won't do anything.
Building your own toggle
If your blog is set to `system` mode and you want to give readers a manual override (rather than relying purely on their OS setting), you can build a small client component using next-themes' own `useTheme()` hook directly — no VeloCMS-specific toggle component ships today, but the underlying library is already a dependency.
"use client";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
export function ReaderThemeToggle() {
const { resolvedTheme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return null; // avoid SSR/client mismatch
return (
<button
type="button"
aria-label="Toggle dark mode"
onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
>
{resolvedTheme === "dark" ? "\u2600\uFE0F" : "\u{1F319}"}
</button>
);
}Drop this into your theme's nav layout (e.g. inside the `<nav className="site-nav">` block). Style it with your theme's own CSS rather than assuming a shared component exists.
Verify with the keyboard
Press Tab through your blog header. Your toggle must receive focus and respond to Space/Enter — a plain `<button>` gets this for free. Give it a real `aria-label` (not just an emoji as the only content) so screen reader users know what it does, and double-check your theme's CSS reset hasn't stripped the focus ring with a stray `outline: none`.