TT5 Dark Mode — The Missing Plugin for WordPress Twenty Twenty-Five

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:

TabWhat it controls
Dark ModePalette selection, Auto mode toggle
Focus & OutlineGlobal and per-element focus styles
Box ShadowShadow preset values, dark mode shadow behavior
Links & HoverHover color, underline style, transition, button opacity
AdvancedCustom 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:

PaletteBaseContrastCharacter
🌆 Evening#1B1B1B#F0F0F0Warm, muted — the safe default
🌃 Twilight#131313#FFFFFFHigh contrast with blue/coral accents
🌌 Midnight#4433A6#79F3B1Bold purple with neon green — distinctive
🌅 Sunrise#330616#FFFFFFDeep 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):

  1. The script reads the tt5dm_pref cookie
  2. It checks prefers-color-scheme if needed
  3. It adds tt5-dark-mode or tt5-light-mode to <html> immediately
  4. 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:

VariantAppearanceBest for
🔘 PillRounded capsule button with text labelHeaders with space for text
🎯 Icon-onlyCircular button with sun/moon iconCompact headers, mobile-first layouts
🎚️ SwitchiOS-style toggle with sliding thumbSettings 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" and aria-checked for screen reader compatibility
  • aria-label that dynamically reflects the current state
  • SVG icons embedded inline (no icon font dependency)
  • The button is the block root — no wrapper <div>, which means useBlockProps alignment 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 :focus instead 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:

SettingOptionsDefault
Focus Mode:focus-visible / :focus / disabled:focus-visible
Outline ColorAny CSS colorTheme accent-4
Outline Width1–5 px2px
Outline Stylesolid / dashed / dottedsolid
Outline Offset0–10 px2px

Element level — Override the global settings for specific element types:

  • 🔘 Buttons — Custom outline color and offset
  • 📝 Form inputs — Custom outline color + optional box-shadow ring (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-visible mode (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:

PresetDefault ValuePurpose
Natural0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.08)Subtle depth — fixes the Noon bug
Soft0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1)Card-level elevation
Hard4px 4px 0 0 currentColorGeometric/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:

SettingWhat it doesDefault
Hover ColorChanges link text color on hovernone (inherits)
Underline Stylesolid / dashed / dotted / none on hovertheme default (dotted)
Transition DurationSmooth animation between states (0–1000 ms)0 ms
Button Hover OpacityControls color-mix() percentage for .wp-block-button__link:hover85% (theme default)

💡 Recommended starting point: Set hover color to your accent color, underline to solid, and transition to 200ms. 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

ComponentMinimum Version
WordPress6.7
PHP7.4
ThemeTwenty Twenty-Five or a child theme based on TT5

📥 Download & Links


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.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *