Dissecting Expanded Dark Theme
with Claude Science and AOSP
There’s a lot of excitement around customization, personalization, accessibility, and giving people more control over how interfaces adapt to their needs. I’m interested in what happens underneath, and dark mode hides a surprising amount of engineering.
A dark theme is more than a binary inversion. Colors can change, but the visual cues and relationships that give an interface meaning still need to carry through. Dark mode might feel like a preference for some people, but it can matter much more for those with low vision or photosensitivity.
For years, whether an app on your phone had dark mode depended on whether its developers built one. Android 10 (2019) introduced Force Dark, letting developers hand that work off to Android, which could generate a dark version from the app’s existing light theme. Then in late 2025, an Android 16 update introduced Expanded Dark Theme, putting that choice in users’ hands. Users can now ask Android to darken apps that still showed up light, even when those apps never provided their own dark theme. That shift toward user agency caught my attention.
I used Claude Science to probe Expanded Dark Theme: I built a light-only Android app, measured what changed across common interface patterns, and traced the results back through Android’s rendering code.
Setup
I tested fourteen interface patterns on one screen, in three versions. The original light theme, the dark version Android generated from it (I’ll call it auto-dark), and a dark theme I built by hand following Material 3, Google’s own design system (hand-built).
What Android preserved
Could the transformation take readable text and make it unreadable? No. In my experiment, everything that passed the WCAG contrast floor in light mode still passed after darkening, and everything I deliberately built broken stayed broken. The ratio behind those verdicts is:
Contrast Ratio = (L_lighter + 0.05) / (L_darker + 0.05)
where L is each color’s relative luminance. White text on the orange banner failed at 2.8 in light mode and 2.7 in auto-dark. Android inherited my accessibility contrasts rather than auditing or changing them.
Color families held too, for the most part. Blues stayed blue, reds stayed red. The one exception was the orange banner, which came out red. The reason turned out to be buried in the rendering code, which comes later.
The next question I had was how close an automatic dark theme gets to one a person actually designed.
Automatic vs hand-built
On plain text and surfaces, the auto-dark theme came remarkably close to the hand-built one, with only minor color differences. On these simpler patterns, the OS came surprisingly close to my hand-built output without seeing a single design token. The gap surfaced in colorful, mid-toned elements (think link, errors, search, and banner); all read 1.8 to 2.5 times better when hand-built.
The brand banner shows the difference. My hand-built Material theme never touched the original orange. It stretched the brand color into a palette of tones, then picked tones by the banner’s role, so the dark scheme gave it a deep brown fill with pale peach text, the orange’s own dark and light ends, passing contrast at 6.6 to 1 by design.
Get Aryan K’s stories in your inbox
Join Medium for free to get updates from this writer.
These role labels, assigned by a developer, never reach Android’s renderer. In the auto-dark case, the banner is just a color, so all Android can do is run it through the darkening math, and it came out at 2.7 to 1 and redder.
The failures clustered around saturated colors and middle grays. To understand why those colors behaved differently, I traced how Android transforms them in the renderer.
Inside the renderer
For ordinary Android UI rendering, the app records drawing instructions that eventually pass through HWUI, Android’s hardware-accelerated UI renderer. Expanded Dark Theme can transform colors in those drawing instructions before they become pixels.
Roughly, Android first decides whether a rendered chunk behaves like foreground or background. Text-containing nodes tend to become foreground and get pushed lighter; backgrounds get pushed darker, and the background guess is a heuristic the source itself labels a crude overlap check. Then one small piece of math picks the new color:
new lightness = min(110 − old lightness, 100)
applied only when it moves in the right direction. That guard is why my mid-grays never moved; a gray at lightness 50 maps to 60, wrong direction, left alone. Left alone is the trap. The gray stays put while everything behind it goes black, which is why the hand-built theme beat auto-dark on my placeholder; a designer re-lightens that gray, and the math never does. Here it is in the source:
SkColor makeDark(SkColor color) {
Lab lab = sRGBToLab(color);
float invertedL = std::min(110 - lab.L, 100.0f);
if (invertedL < lab.L) {
lab.L = invertedL;
return LabToSRGB(lab, SkColorGetA(color));
} else {
return color;
}
}The math runs in CIELAB, a color system from 1976 that describes a color with three numbers:
L = lightness, 0 (black) to 100 (white)
a = position between green (−) and red (+)
b = position between blue (−) and yellow (+)
The system promises that lightness lives in L, so you can change it without touching the color. Android rewrites L and leaves a and b untouched.
That promise creates two problems in practice. First, it isn’t perfectly kept. The math bends for a few hues, orange among them, which is part of why newer systems like OKLab exist. Second, screens work in a fixed color range called sRGB, and when the math asks for a color outside that range, the screen substitutes the nearest one it has.
My orange banner hit both problems. The bend moved it about four degrees toward red. Then the darkened orange fell outside sRGB; there’s no such thing as a dark vivid orange, so the substitute did the remaining ten degrees. The standard bent a little, and the screen couldn’t deliver the rest.
The point
Expanded Dark Theme is a useful fallback, especially for people who need or strongly prefer darker interfaces. But for developers, Google’s old advice stands: ship your own dark theme. Android can transform pixels. It doesn’t know that orange is your brand color, that gray is placeholder text, or that red communicates an error. Rely on auto-dark, and those decisions fall to renderer heuristics, color math, and the limits of sRGB.
For an automatic fallback, the results are surprisingly good. They are still no substitute for a theme designed with meaning in mind.










