TL;DR: For a marketing site that needs a dark CTA section or a midnight manifesto band, skip the theme system. A single CSS class called .tonal-band that re-scopes a handful of custom properties (--bg, --fg, --accent, --line) on itself replaces 80% of what next-themes, data-theme attributes, Tailwind darkMode: 'class', and a ThemeProvider do. Descendants read the same tokens (bg-bg, text-fg, border-line) and inherit the inverted palette automatically. No provider, no variants, no JS. Where it breaks: user-controlled dark mode toggles, full-app theming, or systems with three or more palettes. For one or two dark sections on a marketing site, one class beats a framework.
Most marketing sites have "dark sections" — a band of midnight background and inverted text, often the CTA or the manifesto section. The default way to ship this is a theme system: next-themes, a data-theme attribute, Tailwind's darkMode: 'class', a ThemeProvider, variants on every component. That is the right tool for a site with a user-controlled dark mode toggle.
For a site that just needs a dark section or two, it is massive overkill. Our sites use a single class called tonal-band that re-scopes a few custom properties on itself. The descendant components look up the same tokens and inherit the inverted palette. No provider, no variants, no JS.
Here is the pattern, why it works, and where it breaks down.
The setup: one palette, variables, no toggle
Our globals.css has one palette defined in @theme and mirrored to :root. Descendant Tailwind classes like bg-bg, text-fg, border-line, text-accent all read those tokens.
@theme {
--color-bg: #fafaf7;
--color-fg: #0f0f12;
--color-muted: #6b6b72;
--color-line: #e8e6e0;
--color-accent: #ff5f1f;
--color-accent-ink: #1a1a1d;
/* ... */
}
:root {
--bg: var(--color-bg);
--fg: var(--color-fg);
--muted: var(--color-muted);
--line: var(--color-line);
--accent: var(--color-accent);
/* ... */
}
Every component in the site reads the same set of six or seven CSS variables. This is the key move. Without it, the pattern does not work.
The tonal-band class
One class, on one selector, re-scopes the same variables:
.tonal-band {
--bg: #0b0b0e;
--fg: #f5f3ee;
--muted: #8a8a92;
--line: #1e1e24;
--accent: #ff8a4c;
--accent-ink: #0b0b0e;
background: var(--bg);
color: var(--fg);
}
Because CSS custom properties cascade, any descendant of .tonal-band that reads var(--bg), var(--fg), var(--line), var(--accent) picks up the dark values. The light-mode values outside the band are unaffected.
Wrap a section:
<section className="tonal-band">
<Manifesto />
<BigCTA />
</section>
Everything inside renders in the dark palette. Buttons, borders, muted text, accents — all inherit the re-scoped tokens. No component gets a dark: prefix anywhere.
Why this beats a theme system on a marketing site
Three reasons.
1. No JavaScript. A theme system runs on mount, hydrates a provider, reads local storage, and applies a class. This is 10–20kb of JS and a flash-of-unstyled-content risk. The tonal-band is pure CSS.
2. No component branching. With next-themes + Tailwind's dark: variants, every component grows a second codepath. Twenty components mean twenty dark:bg-x, dark:text-y, dark:border-z pairs. With tonal-band, components read tokens and do not know or care what the current band is.
3. You can nest inversions. A light callout inside a dark section? Add a class that re-scopes the variables back to the light palette. The cascade handles it. Theme systems struggle here; you end up writing custom wrappers.
Where this breaks down
Two failure modes.
1. When you want a real user-controlled dark mode. If visitors are toggling between light and dark site-wide, a theme system with data-theme on the <html> and a toggle button is the right tool. The tonal-band is author-controlled, not user-controlled.
2. When components hard-code colors instead of reading tokens. The pattern depends on every component reading var(--bg) / var(--fg) etc. If a button has background: #ff5f1f hard-coded, it stays orange in the dark band because it never looked up the token. Discipline on token usage is the prerequisite.
What you need to make this work on your site
Four things.
-
A small, fixed set of tokens. Six to ten CSS variables cover most marketing needs: bg, fg, muted, line, card, accent, accent-ink. More than ten and the scope management gets annoying.
-
Every component reads tokens. No hex codes anywhere except in the
@themeblock and the tonal-band overrides. This is a lint rule we enforce. -
A single class per band. We have
tonal-bandfor our dark band. If you want multiple bands (midnight, ocean, forest), addtonal-band--oceanetc. Do not make it a prop-driven system. -
Tailwind v4 mapping (if you use Tailwind). Tailwind v4 reads
@themedirectly.bg-bg,text-fg,border-linework out of the box with the variables above. That is the other half of why this is clean.
Common questions
Does this work with dark: variants? Yes, but you should not need them. The whole point is to not branch at the component level.
Does this break design tools like Figma? Figma has no idea. You design the two palettes as separate modes in Figma variables, implement them as two token sets in CSS, and the developer pattern is the tonal-band.
Can I animate between the bands? You can animate the variables with transition: background 300ms, color 300ms on the element, but it will look weird because fg/bg flip in unison. For section transitions, we use scroll-triggered entry rather than color interpolation.
Does this work in MDX or a CMS? Yes. Add a tonal-band class to whatever the CMS wraps your section in. No component-level changes needed.
When we use it
On roughly half of our sites. Any project with a distinct CTA band, a manifesto section, or a dark footer treatment uses this pattern. Projects that have a full user-facing dark mode toggle use a theme system. Projects that have neither do not need this at all — a single palette is fine.
The broader point
The bigger lesson behind this is that marketing sites do not need app-scale abstractions. A ThemeProvider solves a real problem when you have a user-controlled theme. It solves no problem when all you want is a dark section. Scoped custom properties were built for exactly this. Use the platform.
A lot of our stack is choices like this: native CSS features over runtime libraries, scoped overrides over component prop drilling, one-purpose classes over configurable systems. Every one of them saves a few kilobytes, a few dependencies, and a few future-maintenance headaches. Over the span of a site, it adds up.