Add dark / light mode switching to WordPress Twenty Twenty-Five — plus global focus fixes, shadow presets, and link hover customization.
Overview
TT5 Dark Mode is a companion plugin for the WordPress Twenty Twenty-Five theme. It adds what the stock theme is missing:
| Problem | Solution |
|---|---|
| No way for visitors to switch between dark and light mode | Cookie-persisted toggle with zero FOUC |
Crude :focus outlines (no color, no offset, no :focus-visible) | Comprehensive, customizable focus system |
Broken var:preset|shadow|natural reference in Noon variation | Shadow presets injected via wp_theme_json_data_theme |
| Link hover only changes underline to dotted — no color or animation | Configurable hover color, underline style, and transition |
Everything is managed through a tabbed settings panel under Settings → TT5 Dark Mode.
Features
Dark / Light Mode
- Two-state toggle — Dark ↔ Light (default).
- Three-state cycle — Auto → Dark → Light, honoring
prefers-color-scheme. - Four official palettes — Evening, Twilight, Midnight, Sunrise.
- Smart inversion — Automatically provides a light-mode switch when the active style variation is already dark.
- Zero FOUC — Inline
<head>script reads the cookie before first paint. - Cookie persistence —
tt5dm_pref, SameSite=Lax, Secure flag on HTTPS.
Gutenberg Blocks
| Block | Description |
|---|---|
| Dark Mode Toggle | Styleable button (pill / icon-only / switch). Full block support: color, spacing, typography, border. |
| Mode-Aware Content | Container that shows its children only in dark or light mode. Pure CSS — no JS on the frontend. |
Block Patterns: Header with Toggle · Mode-Aware Hero Section · Mode-Aware Logo
Focus & Outline System
- Global
:focus-visiblewith color, width, style, and offset controls. - Element-level overrides for buttons, form inputs, and navigation.
- Separate outline color for dark mode.
- Three modes:
focus-visible(recommended) /focus/ disabled.
Box Shadow Presets
- Natural, Soft, and Hard presets — appear in the Gutenberg Shadow picker.
- Fixes the broken
var:preset|shadow|naturalreference. - Dark mode adjustment: Inherit / Darken / Glow.
Link & Hover
- Hover color, underline style, transition duration (0–1000 ms).
- Button hover opacity via
color-mix().
Custom CSS
- Inject arbitrary CSS with mode-specific selectors (
html.tt5-dark-mode,html.tt5-light-mode).
Installation
From ZIP
- Download the latest release
.zipfile. - In WordPress, go to Plugins → Add New → Upload Plugin and upload the zip.
- Activate the plugin.
Manual
cd wp-content/plugins/
git clone https://github.com/satoshiwp/tt5-dark-mode.git
Activate through the WordPress admin.
Configuration
- Settings → TT5 Dark Mode — Configure palette, focus, shadows, links, and custom CSS.
- Site Editor → Header template — Insert the Dark Mode Toggle block.
Architecture
index.php (main plugin file)
│
├── [FOUC inline script] ← <head> priority 1, reads cookie, sets html class
│
├── [Frontend CSS] ← wp_add_inline_style: palette + focus + links + custom
│
├── [Shadow presets] ← wp_theme_json_data_theme → Gutenberg Shadow picker
│
├── dark-mode-core.js ← Singleton state manager, loaded on-demand via viewScript
│ └── view.js ← Toggle block frontend interactivity
│
├── blocks/toggle/ ← Dark Mode Toggle Gutenberg block
│ ├── block.json
│ ├── edit.js ← Editor component (no build step, raw createElement)
│ ├── render.php ← Server-side render (<button> = block root)
│ ├── view.js ← Frontend JS (depends on core.js)
│ ├── style.css ← Shared styles
│ └── editor.css ← Editor-only styles
│
├── blocks/mode-content/ ← Mode-Aware Content Gutenberg block
│ ├── block.json
│ ├── edit.js
│ ├── render.php
│ ├── style.css ← Pure CSS visibility switching
│ └── editor.css
│
├── assets/css/admin.css ← Admin settings page styles
├── assets/js/admin.js ← Tab switching, color swatches, shadow preview
│
└── includes/blocks.php ← Block registration, patterns, editor data injection
For Developers
Filters
// Customize the dark palette programmatically
add_filter( 'tt5dm_dark_palette', function ( $palette, $selected_key ) {
$palette['colors']['accent-1'] = '#FF6600';
return $palette;
}, 10, 2 );
CSS Custom Properties
/* Global focus variables (set by plugin settings) */
:root {
--tt5c-focus-color: #007cba;
--tt5c-focus-width: 2px;
--tt5c-focus-style: solid;
--tt5c-focus-offset: 2px;
}
/* Toggle-specific focus variables (override per-block) */
:root {
--tt5dm-focus-color: currentColor;
--tt5dm-focus-width: 2px;
--tt5dm-focus-offset: 2px;
}
JavaScript API
// The global state object (available after core.js loads)
window.__tt5dm.getPref(); // 'auto' | 'dark' | 'light'
window.__tt5dm.isDark(); // boolean — current computed state
window.__tt5dm.cycle(); // Advance to next preference
window.__tt5dm.apply('dark'); // Set a specific preference
// Listen for changes
document.addEventListener('tt5dm:change', (e) => {
console.log(e.detail.pref); // 'auto' | 'dark' | 'light'
console.log(e.detail.isDark); // boolean
});
Mode-Specific CSS Selectors
html.tt5-dark-mode { /* dark mode active */ }
html.tt5-light-mode { /* light mode active */ }
/* Example: different background image per mode */
html.tt5-dark-mode .hero { background-image: url(hero-dark.jpg); }
html.tt5-light-mode .hero { background-image: url(hero-light.jpg); }
Requirements
| Requirement | Version |
|---|---|
| WordPress | 6.7+ |
| PHP | 7.4+ |
| Theme | Twenty Twenty-Five (or child theme) |
Technical Details
- No build step — Editor scripts use
wp.element.createElementdirectly. - No dependencies — No jQuery, no external frameworks.
- Under 8 KB total — All files combined.
- Inline CSS only — Zero extra HTTP requests for styles.
- Full i18n — All strings use the
tt5-dark-modetext domain. - WordPress Coding Standards — Follows WPCS conventions.
License
This project is licensed under the GNU General Public License v2.0 or later.
TT5 Dark Mode — Making Twenty Twenty-Five comfortable at any hour.