3 of 10 colors failed Safari's dark-mode contrast bar

2026-11-04来源 dev.to CSSEnglish

My toy version of Safari's Adaptive Dark Mode failed 3 of 10 text colors on WCAG's 4.5:1 contrast bar. Naive RGB inversion passed 9 of 10 — but it turned every blue link orange.

Apple announced Adaptive Dark Mode in Safari 27.3 beta on November 3, and the WebKit blog post is unusually candid about the mechanism: when a page doesn't declare dark-mode support, Safari analyzes the page, and if it looks predominantly light, it transforms colors with a matrix that inverts luminosity while preserving hue, aiming for the WCAG 4.5:1 contrast ratio for text. Almost 40% of page loads come from users in dark mode, WebKit says, while only 10% of page loads support it — that gap is the whole product. I couldn't run Safari 27.3 beta here, so I built the smallest possible version of the described transform and measured where it breaks.

The experiment is 30 lines of Python, standard library only. I took a 12-color palette of typical light-mode UI colors and applied two transforms to each: naive inversion (255 - c per channel) and luminosity inversion (convert to HLS, replace lightness L with 1 - L, keep hue and saturation). Then I computed WCAG relative-luminance contrast against the transformed background (white becomes black under both transforms) and the hue drift in degrees.

import colorsys def lum(rgb): def f(c): return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4 r, g, b = (f(c) for c in rgb) return 0.2126 * r + 0.7152 * g + 0.0722 * b def contrast(a, b): la, lb = lum(a), lum(b) return (max(la, lb) + 0.05) / (min(la, lb) + 0.05) def naive(rgb): return tuple(1 - c for c in rgb) def lum_inv(rgb): h, l, s = colorsys.rgb_to_hls(*rgb) return colorsys.hls_to_rgb(h, 1 - l, s) 
color orig naive hueDriftN lumInv hueDriftL contrastN contrastL body text #1A1A1A #e5e5e5 0.0 #e5e5e5 0.0 16.67 16.67 secondary text #666666 #999999 0.0 #999999 0.0 7.37 7.37 link blue #0066CC #ff9933 180.0 #3399ff 0.0 9.86 7.14 accent orange #E8590C #17a6f3 180.0 #f36417 0.0 7.79 6.65 success green #2F9E44 #d061bb 180.0 #61d076 0.0 6.11 10.79 danger red #D6336C #29cc93 180.0 #cc2962 0.0 10.15 4.06 warning yellow #F08C00 #0f73ff 180.0 #ff9b0f 0.0 4.92 9.95 muted gray #868E96 #797169 180.0 #697179 0.0 4.38 4.24 button blue #1971C2 #e68e3d 180.0 #3d95e6 0.0 8.30 6.64 code red #C92A2A #36d5d5 180.0 #d53636 0.0 11.64 4.41 

Counting only the 10 text colors (excluding the page background and the border gray), naive inversion passes 4.5:1 on 9 of 10, luminosity inversion on 7 of 10. The three luminosity-inversion failures are danger red at 4.06, muted gray at 4.24, and code red at 4.41 — all just under the bar, which is exactly the kind of near-miss a real implementation has to catch and correct. Mean hue drift tells the other half of the story: 135.0 degrees for naive inversion across the full 12-color palette, 0.0 degrees for luminosity inversion. Naive inversion wins on contrast and destroys identity — your blue link becomes orange, your green success message becomes magenta. Luminosity inversion keeps every hue and pays for it in contrast.

Two edge cases sharpen the point. Pure red #FF0000 sits at lightness 0.5, so luminosity inversion leaves it exactly unchanged — still red, still 5.25:1 against black, but a red that was designed for a white page now shouts on a dark one. And for achromatic colors the two transforms are identical: body text #1A1A1A becomes #e5e5e5 either way, at 16.67:1. The hard cases are all saturated colors near the middle of the lightness range, which is to say, most of a real design system.

So WebKit's "aims for sufficient contrast as defined by the WCAG 4.5:1 ratio" has to mean more than a pure lightness mirror. A mirror alone fails 3 of my 10 text colors. The real implementation must push failing text colors further — lighter or darker than the mirror position — or it would ship the exact failures my toy produces. The blog doesn't publish that correction step, but my numbers say it must exist.

I also checked the opt-out side, because that's where this feature meets real sites. Pages that declare dark support — <meta name="color-scheme" content="light dark"> or color-scheme: light dark in CSS — are left untouched. I fetched 9 homepages and grepped the served HTML: 5 clearly declare dark support (Wikipedia's inline dark CSS, GitHub's light dark meta tag, MDN, Node.js, and Tailwind), 3 showed no declaration (python.org, mozilla.org, and Stack Overflow's redirect page), and React's page had a single ambiguous hit. That 5-of-9 is not a rebuttal of WebKit's web-wide 10% figure — my sample is dev-centric sites that already care about dark mode, and HTML-only grepping misses declarations in external stylesheets, so it's a lower bound. It's a reminder that the sites Adaptive Dark Mode will transform most are the ones least likely to have thought about it.

Three details from the blog post are worth pocketing. First, <canvas> content is left unchanged, and effects like mix-blend-mode are sometimes switched off when they would fight the transform — so a page can look dark and still have light islands. Second, JavaScript that queries CSS colors gets the original authored colors, not the transformed ones, and canvas draws render in original form: you cannot feature-detect Adaptive Dark Mode from script, so testing it takes eyes, not code. Third, the opt-out has a catch: color-scheme: light only is the declared way to refuse transformation, but the blog notes the only keyword is not supported in Safari 27.3 beta 1. Today, the working escape is shipping a real dark mode — which is presumably the point.

Sources: WebKit blog, "Introducing Adaptive Dark Mode" (November 3, 2026) — https://webkit.org/blog/18541/introducing-adaptive-dark-mode/ . Palette, transforms, and contrast numbers are my own measurements, run locally; the site audit is HTML-only and a lower bound, as noted in the text.

原文:https://www.a8q9.cn/en/article/3-of-10-colors-failed-safari-s-dark-mode-contrast-bar-abd1f4.html

相关文章