TT5 Dark Mode is a Gutenberg-native plugin that adds dark/light mode switching, focus fixes, shadow presets, and link hover customization to WordPress’s default theme.
Twenty Twenty-Five is one of the most refined default themes WordPress has ever shipped. Its style variation system, fluid typography, and minimal footprint make it an excellent foundation for a wide range of websites.
But after building several production sites on TT5, I kept running into the same four friction points — issues that couldn’t be solved with Additional CSS alone and that no existing plugin addressed as a cohesive package.
TT5 Dark Mode was born from that frustration. It’s a single, focused plugin that fills the gaps TT5 left behind.
🔍 The Four Problems This Plugin Solves
Before diving into features, let’s be specific about what’s actually missing in Twenty Twenty-Five and why each gap matters.
Problem 1 — No dark mode toggle for visitors.
TT5 includes gorgeous dark palettes (Evening, Twilight, Midnight, Sunrise) as style variations, but these are design-time choices made by the site owner. Visitors have no way to switch between dark and light mode on the frontend. In 2025, dark mode isn’t a luxury — it’s a baseline accessibility and comfort expectation.
Problem 2 — Crude focus outlines.
TT5 applies a global :focus rule to all interactive elements with no color customization, no outline offset, and — critically — no :focus-visible distinction. This means every mouse click on a button or link triggers a visible outline ring, which is visually distracting for mouse users and violates the modern UX standard where outlines should only appear during keyboard navigation.
Problem 3 — Broken shadow reference in the Noon variation.
TT5’s “Noon” style variation references var:preset|shadow|natural in its theme.json, but never actually defines the preset. The result: any block that relies on this shadow token (buttons, cards) renders with no shadow at all. This is a confirmed upstream bug.
Problem 4 — Minimal link hover feedback.
When a visitor hovers over a link in TT5, the only visual change is the underline switching from solid to dotted. There’s no color shift, no transition animation, and no way to customize this behavior through the Site Editor. For sites that depend on clear visual affordances, this is insufficient.
TT5 Dark Mode solves all four problems through a single tabbed settings panel and two Gutenberg blocks, with zero external dependencies and under 8 KB of total code.
⚡ Quick Start — Up and Running in 3 Minutes
Getting the plugin working requires exactly four steps. No build tools, no configuration files, no terminal commands.
Step 1 — Install the plugin.
Upload tt5-dark-mode.zip via Plugins → Add New → Upload Plugin, or extract the tt5-dark-mode folder into /wp-content/plugins/ manually.
Step 2 — Activate.
Go to Plugins → Installed Plugins and click Activate next to “TT5 Dark Mode.”
💡 If your active theme is not Twenty Twenty-Five (or a child theme of TT5), the plugin will display a notice and gracefully disable all its frontend features. It will not break your site.
Step 3 — Configure settings.
Navigate to Settings → TT5 Dark Mode. The settings page is organized into five tabs:
| Tab | What it controls |
|---|---|
| Dark Mode | Palette selection, Auto mode toggle |
| Focus & Outline | Global and per-element focus styles |
| Box Shadow | Shadow preset values, dark mode shadow behavior |
| Links & Hover | Hover color, underline style, transition, button opacity |
| Advanced | Custom CSS injection, legacy toggle focus overrides |
For most sites, the default settings work immediately — you only need to choose a dark palette and optionally enable Auto mode. Everything else is fine-tuning.
Step 4 — Place the toggle block.
Open the Site Editor (Appearance → Editor), navigate to your header template, and insert the Dark Mode Toggle block. Choose your preferred variant — pill, icon-only, or switch — and save.
That’s it. Your visitors can now switch between dark and light mode, and their preference persists via a cookie.
🌙 Feature Deep Dive: Dark / Light Mode Switching
This is the plugin’s flagship feature, and it’s designed to handle every edge case correctly.
How the toggle works
The toggle button cycles between states:
- Default (two-state): Dark ↔ Light
- With Auto enabled (three-state): Auto → Dark → Light → Auto
In Auto mode, the plugin respects the visitor’s operating system preference via the prefers-color-scheme media query. If the visitor’s OS switches from light to dark (e.g., at sunset with scheduled dark mode), the site updates in real time — no page reload required.
How palettes are applied
Under the hood, the plugin doesn’t repaint the page or swap stylesheets. Instead, it overrides TT5’s CSS custom properties:
--wp--preset--color--base
--wp--preset--color--contrast
--wp--preset--color--accent-1
--wp--preset--color--accent-2
--wp--preset--color--accent-3
--wp--preset--color--accent-4
--wp--preset--color--accent-5
--wp--preset--color--accent-6
Because every TT5 block references these variables, the entire page — headers, footers, buttons, text, backgrounds — adapts automatically when the mode changes. No per-block styling is needed.
The four available dark palettes
All palettes are sourced directly from TT5’s official style variations, ensuring visual consistency:
| Palette | Base | Contrast | Character |
|---|---|---|---|
| 🌆 Evening | #1B1B1B | #F0F0F0 | Warm, muted — the safe default |
| 🌃 Twilight | #131313 | #FFFFFF | High contrast with blue/coral accents |
| 🌌 Midnight | #4433A6 | #79F3B1 | Bold purple with neon green — distinctive |
| 🌅 Sunrise | #330616 | #FFFFFF | Deep burgundy with warm yellow tones |
Smart inversion for dark-based style variations
Here’s a nuance most dark mode plugins get wrong: what happens when the site’s default style is already dark?
TT5 Dark Mode detects the base color luminance of the active style variation using the WCAG 2.1 relative luminance formula. If the base color is dark (luminance < 0.4), the plugin automatically inverts its entire logic:
- The toggle button label changes from “Dark mode” to “Light mode”
- The “alternate” palette becomes the default TT5 light palette
- Cookie values and CSS classes still work identically
This means if you’re using TT5’s “Evening” style variation as your default, the plugin gives your visitors a light mode switch — not a redundant dark mode one.
📌 This detection happens server-side and is cached with a static variable, so there’s zero performance overhead.
Zero FOUC (Flash of Unstyled Content)
The most common complaint with JavaScript-based dark mode solutions is the “flash” — the page briefly renders in light mode before JavaScript kicks in and switches to dark.
TT5 Dark Mode eliminates this entirely with a synchronous inline script injected at wp_head priority 1 (before any stylesheets load):
- The script reads the
tt5dm_prefcookie - It checks
prefers-color-schemeif needed - It adds
tt5-dark-modeortt5-light-modeto<html>immediately - The CSS that overrides the palette variables is scoped to these classes
Because the class is set before the browser’s first paint, there is literally no frame where the wrong palette is visible.
🧱 Feature Deep Dive: Gutenberg Blocks
The plugin registers two blocks, both fully Gutenberg-native — no shortcodes, no widgets, no legacy code.
Dark Mode Toggle Block
Where to use it: Header template, sidebar, footer, or any page/post content.
Three variants:
| Variant | Appearance | Best for |
|---|---|---|
| 🔘 Pill | Rounded capsule button with text label | Headers with space for text |
| 🎯 Icon-only | Circular button with sun/moon icon | Compact headers, mobile-first layouts |
| 🎚️ Switch | iOS-style toggle with sliding thumb | Settings panels, preference sections |
All three variants support full block styling: custom colors, spacing, typography, border radius, and alignment. The block integrates with WordPress’s native block controls — no custom sidebar panels.
What the block renders on the frontend:
A single <button> element with:
role="switch"andaria-checkedfor screen reader compatibilityaria-labelthat dynamically reflects the current state- SVG icons embedded inline (no icon font dependency)
- The button is the block root — no wrapper
<div>, which meansuseBlockPropsalignment works natively
Mode-Aware Content Block
This is the less obvious but equally powerful block. It’s a container that conditionally shows its children based on the current mode.
Use cases:
- 🖼️ Show a dark-background hero image in dark mode and a light-background one in light mode
- 📝 Display different welcome messages per mode
- 🎨 Swap logos (dark logo on light backgrounds, light logo on dark backgrounds)
How it works:
The visibility is controlled entirely with CSS — no JavaScript on the frontend. The block renders a <div> with a data-mode="dark" or data-mode="light" attribute, and the stylesheet uses these rules:
html.tt5-light-mode [data-tt5dm-mode="dark"] { display: none; }
html.tt5-dark-mode [data-tt5dm-mode="light"] { display: none; }
This means mode-aware content works instantly on page load (no JS delay) and is fully compatible with caching plugins.
Block Patterns
The plugin includes three ready-to-use patterns to get you started:
- 📐 Header with Toggle — A navigation row with the toggle placed in the right column
- 🎨 Mode-Aware Hero Section — Two stacked hero sections, one visible per mode
- 🖼️ Mode-Aware Logo — A pair of image blocks for light/dark logo variants
Access these via the Block Inserter → Patterns → TT5 Dark Mode category in the Site Editor.
🎯 Feature Deep Dive: Focus & Outline System
This feature alone justifies installing the plugin, even if you don’t need dark mode.
What’s wrong with TT5’s default focus
TT5 applies this rule globally:
:where(.wp-site-blocks) *:focus {
outline: ...;
}
The problems:
- ❌ Uses
:focusinstead of:focus-visible, so mouse clicks trigger outlines - ❌ No customizable outline color (defaults to browser UA style)
- ❌ No outline offset (the ring hugs the element edge)
- ❌ Same behavior for all element types (buttons, inputs, nav links)
- ❌ No dark/light mode differentiation
What TT5 Dark Mode provides
The plugin replaces this with a layered focus system:
Global level — Applies to all interactive elements within .wp-site-blocks:
| Setting | Options | Default |
|---|---|---|
| Focus Mode | :focus-visible / :focus / disabled | :focus-visible |
| Outline Color | Any CSS color | Theme accent-4 |
| Outline Width | 1–5 px | 2px |
| Outline Style | solid / dashed / dotted | solid |
| Outline Offset | 0–10 px | 2px |
Element level — Override the global settings for specific element types:
- 🔘 Buttons — Custom outline color and offset
- 📝 Form inputs — Custom outline color + optional
box-shadowring (the common “glow” pattern used by most design systems) - 🧭 Navigation links — Custom outline offset (TT5 uses different offsets for parent items vs. submenus)
Dark mode level — Use a different outline color when dark mode is active. This is critical because a dark blue outline that’s visible on white backgrounds becomes invisible on dark backgrounds.
💡 Tip: When using the
:focus-visiblemode (recommended), the plugin also adds:focus:not(:focus-visible) { outline: none !important; }to ensure mouse clicks produce absolutely no outline artifact. This is the behavior that Chrome, Firefox, and Safari all default to in their native UI.
🎨 Feature Deep Dive: Box Shadow Presets
The Noon variation bug
TT5’s “Noon” style variation includes this in its theme.json:
"shadow": "var:preset|shadow|natural"
But the natural shadow preset is never defined anywhere in TT5’s theme files. The result: buttons and blocks that reference this token render with box-shadow: none.
How the plugin fixes it
TT5 Dark Mode injects three shadow presets into the theme.json data using WordPress’s wp_theme_json_data_theme filter:
| Preset | Default Value | Purpose |
|---|---|---|
| Natural | 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.08) | Subtle depth — fixes the Noon bug |
| Soft | 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1) | Card-level elevation |
| Hard | 4px 4px 0 0 currentColor | Geometric/brutalist accent |
All three values are editable in the settings panel with a live preview box that updates as you type. They also appear in the Gutenberg Shadow picker for any block that supports box-shadow.
Dark mode shadow adjustment
Standard shadows — dark shapes on a dark background — are nearly invisible. The plugin provides three strategies:
- Inherit — Same shadows in both modes (default)
- Darken — Increases shadow opacity for better definition on dark backgrounds
- Glow — Replaces dark shadows with subtle white light halos
🔗 Feature Deep Dive: Link & Hover Customization
TT5’s default link hover behavior is a single CSS change: the underline style switches from solid to dotted. There’s no color shift and no animation.
The plugin adds four controls:
| Setting | What it does | Default |
|---|---|---|
| Hover Color | Changes link text color on hover | none (inherits) |
| Underline Style | solid / dashed / dotted / none on hover | theme default (dotted) |
| Transition Duration | Smooth animation between states (0–1000 ms) | 0 ms |
| Button Hover Opacity | Controls color-mix() percentage for .wp-block-button__link:hover | 85% (theme default) |
💡 Recommended starting point: Set hover color to your accent color, underline to
solid, and transition to200ms. This provides clear, polished hover feedback without being distracting.
⚙️ Feature Deep Dive: Advanced Tab
Custom CSS
The Advanced tab includes a code editor for injecting arbitrary CSS. This CSS loads after all plugin-generated styles, so it can override anything.
The key selectors you’ll use most often:
/* Target dark mode only */
html.tt5-dark-mode .my-element {
background: #1a1a2e;
}
/* Target light mode only */
html.tt5-light-mode .my-element {
background: #fafafa;
}
/* Target the moment before JS initializes (edge case) */
html:not(.tt5-dark-mode):not(.tt5-light-mode) .my-element {
/* Shown only if cookie and JS both fail */
}
Legacy Toggle Focus (backward compatibility)
If you were using an earlier development version of this plugin, toggle-specific focus settings are preserved here. For new installations, the global Focus & Outline system (Tab 2) is the recommended approach.
🧩 For Theme Developers
Available filter
// Modify the dark palette before CSS is generated
add_filter( 'tt5dm_dark_palette', function( $palette, $key ) {
// $key is 'evening', 'twilight', 'midnight', or 'sunrise'
if ( 'evening' === $key ) {
$palette['colors']['accent-1'] = '#FF6600';
}
return $palette;
}, 10, 2 );
CSS custom properties
The plugin exposes two sets of custom properties:
Global focus properties — set by the Focus & Outline tab:
--tt5c-focus-color
--tt5c-focus-width
--tt5c-focus-style
--tt5c-focus-offset
Toggle-specific focus properties — set by the Legacy section:
--tt5dm-focus-color
--tt5dm-focus-width
--tt5dm-focus-offset
Both can be overridden in your child theme’s style.css or via the Additional CSS panel in the Customizer.
JavaScript API
The plugin exposes a global state manager on window.__tt5dm:
window.__tt5dm.getPref() // → 'auto' | 'dark' | 'light'
window.__tt5dm.isDark() // → boolean
window.__tt5dm.cycle() // Advance to next state
window.__tt5dm.apply('dark') // Force a specific state
// Listen for mode changes
document.addEventListener('tt5dm:change', (e) => {
console.log(e.detail.pref, e.detail.isDark);
});
❓ Frequently Asked Questions
Does this work with themes other than Twenty Twenty-Five?
No. The plugin checks wp_get_theme()->get_template() on every page load and disables all features if the template is not twentytwentyfive. It will not break other themes — it simply does nothing.
Is the cookie GDPR-compliant?
The plugin stores a single functional cookie (tt5dm_pref) that records the visitor’s display preference. Under GDPR and ePrivacy Directive guidance, functional cookies that serve accessibility or preference purposes are generally exempt from consent requirements. That said, if your privacy policy lists all cookies, you should include this one.
Does it work with caching plugins?
Yes. The dark/light mode switching is handled entirely on the client side (cookie + inline script + CSS classes). The server delivers the same HTML regardless of the visitor’s mode preference, so full-page caching works without any special configuration.
What happens if JavaScript is disabled?
The site renders in its default style (whatever the active style variation is). The toggle block renders as a <button> element but has no click handler. The inline <head> script also won’t run, so no CSS class is added to <html>. The page is fully functional — just without the ability to switch modes.
Can I use this with WooCommerce?
Yes, as long as WooCommerce is running on a TT5-based theme. WooCommerce blocks inherit TT5’s CSS custom properties, so they’ll adapt to dark/light mode automatically. Custom WooCommerce templates that hardcode colors may need additional CSS via the Advanced tab.
How lightweight is this plugin?
- 📦 Total file size: under 8 KB (all PHP, JS, and CSS combined)
- 🔌 Zero external dependencies (no jQuery, no frameworks)
- 📡 Zero extra HTTP requests for styles (all CSS is inline)
- 🧠 No database queries beyond a single
get_option()call per page load
📋 Requirements
| Component | Minimum Version |
|---|---|
| WordPress | 6.7 |
| PHP | 7.4 |
| Theme | Twenty Twenty-Five or a child theme based on TT5 |
📥 Download & Links
- GitHub: github.com/satoshiwp/tt5-dark-mode
- WordPress.org: (link to plugin page after approval)
- License: GPLv2 or later
TT5 Dark Mode is free, open-source, and built for the community. If you find a bug or have a feature request, please open an issue on GitHub. Pull requests are welcome.
Leave a Reply