Theme Sync ? Server-Side UI Preference Detector
Detect a visitor's system UI preferences ? dark mode, contrast, reduced
motion, reduced transparency? on theserver, before the first byte
of HTML is sent, and render the correct theme with zero flash of the
wrong theme (FOUC/FOIT), even on the very first page load.
The problem
The usual way to support prefers-color-scheme in a PHP app is:
render a default (usually light) theme, then run client-side JS that
reads matchMedia() and swaps a class or re-fetches CSS. Every single
page load flashes the wrong theme for a fraction of a second, because
the server has no idea what the browser prefers until JS runs.
The approach
This package combines two detection strategies so PHP can know the
answer before rendering anything:
-
HTTP Client Hints (`Sec-CH-Prefers-Color-Scheme`, `-Contrast`,
`-Reduced-Motion`, `-Reduced-Transparency`). Once the site opts in,
supporting browsers attach these headers to every request
automatically ? no JS, no cookie, no flash, ever.
-
A first-party JSON cookie, written by a tiny synchronous inline
script on browsers that don't support Client Hints yet. On the very
first visit there's nothing to read yet, so the same script also
applies the theme immediately client-side (before paint), while
storing the cookie so every request from then on is rendered
correctly by PHP alone ? no JS required afterward, works even with
JS later disabled.
Client Hints, when present, always take priority since they cost zero
round-trips and zero JavaScript.
Installation
Copy src/ThemePreferenceDetector.php into your project, or require
it directly. No dependencies.
Usage
require 'src/ThemePreferenceDetector.php';
use ThemeSync\ThemePreferenceDetector;
$theme = new ThemePreferenceDetector();
$theme->sendAcceptCHHeader(); // opt in to Client Hints for next visit
echo '<html' . ($theme->isDarkMode() ? ' class="dark"' : '') . '>';
echo '<style>' . $theme->toCssVariables() . '</style>';
echo $theme->getBootstrapScript(); // put in <head>, only needed once
See examples/example.php for a full working page, runnable with:
php -S localhost:8000 -t examples
curl -H "Sec-CH-Prefers-Color-Scheme: dark" http://localhost:8000/example.php
API
| Method | Description |
|---|---|
| getPreference(string $key) | Raw value for color-scheme, contrast, reduced-motion, reduced-transparency |
| isDarkMode() | bool |
| prefersReducedMotion() | bool |
| prefersMoreContrast() | bool |
| prefersReducedTransparency() | bool |
| all() | array of all resolved preferences |
| toCssVariables(?array $map) | renders a :root { --var: value; } block |
| getBootstrapScript() | inline <script> for the cookie fallback |
| sendAcceptCHHeader() | opts in to Client Hints via response header |
| getAcceptCHMetaTag() | opts in via <meta> tag instead |
| wasDetectedFromClientHints() / wasDetectedFromCookie() / isUsingDefaults() | introspection on how the result was obtained |
Browser support
Client Hints are currently Chromium-only (Chrome, Edge, Opera, etc.).
Firefox and Safari always fall through to the cookie strategy, which
works everywhere matchMedia() is supported ? i.e. virtually every
browser since 2015.
Tests
php tests/run-tests.php
License
MIT ? see LICENSE.