Skip to content

Comparison & synthesis

Where the field agrees, where it genuinely forks, and where Sparkles stands today. Read the deep-dives first if you want the evidence; this page is the conclusions.

Last reviewed: August 9, 2026


At a glance

PlatformSchemeAccentContrastReduced motionPush notifyShips a paletteReachable without the native toolkit
iOS / iPadOS3-valuednone2-valuedno — UIKit traits only
Android3-valuedwallpaper-seeded3-valued✓ (5×13)scheme only; the rest needs JNI
GNOME3-valued9 named2-valued✓ (patchy)yes — D-Bus portal
KDE Plasma3-valuedfree RGB— (native only)✓ (8 role sets)yes — INI + portal
Windows2-valuedfree RGB + ladderforced colorspartly — registry yes, accent no
macOS2-valued8 indexed2-valued✓ (racy)yes — global-domain defaults
Terminal2-valuednonenonethe 16-color paletteyes — two escape sequences
Web2-valuednone4-valuedn/a — it is the sandbox

Three structural facts fall straight out of this table.

Nobody is missing the scheme, and almost everybody is missing something else. Light/dark is universal and solved. Accent is absent on iOS, the terminal and the web; contrast is absent on the terminal and unreachable neutrally on KDE. A consumer abstraction that models "appearance support" as one bit cannot describe a single row here honestly.

Push notification is universal. Every platform, including the terminal over a pty, can tell an application that the appearance changed. Polling is never required and never correct. The corollary is that the ability to re-theme at runtime is the real requirement — reading the preference at startup is the easy half.

Reachability from outside the native toolkit is better than expected. Four of eight are fully reachable with no toolkit link at all, and the two hardest cases are the two Apple mobile-style APIs. For a self-rendering application like hue, that is the difference between "we can do this" and "we need a UIKit shim per platform".


Dimension 1 — the color scheme

Consensus. Three values (light / dark / no preference), delivered by push, re-read on notification. Every platform that got here later (Windows, the web, the terminal, macOS) shipped two values and regretted it in a small way: the app's own default becomes unexpressible.

The fork: semantic answer vs. inferred one. Windows has no IsDarkMode; the documented method infers it from a color's brightness. The terminal had the same shape until DEC mode 2031, and OSC 11 remains the fallback. Everyone else answers semantically.

The measured consequence is in color-derivation: the three brightness formulas in play (Rec. 601, Windows' (5G+2R+B)/8, CIE L*) disagree only in a narrow band of mid greys that no real theme uses. So inference is not inaccurate — it is underdetermined. A fixed theme has a fixed background, so inferring from it reports what the theme decided, never what the user wants. That is the whole case for asking the platform.

Resolution for Sparkles. Model three values. Prefer the semantic answer; fall back to inference only where nothing else exists, and keep schemeForBackground for that role rather than replacing it.

Dimension 2 — accent: raw or quantized

A genuine three-way fork, with a clear line through it:

ApproachPlatformsWhy
Free RGBKDE, Windows, Android (seed)maximum user expression
Quantized to a named setGNOME (9), macOS (8), libadwaita (9 internally)lets a toolkit ship hand-tuned assets per color
Not exposediOS, terminal, webapp identity (Apple) or fingerprinting (web)

libadwaita does something revealing: it consumes free RGB on Windows, macOS and Android and immediately re-quantizes it with adw_accent_color_nearest_from_rgba(), discarding the user's actual choice — because its widgets want to special-case nine colors. That is a defensible trade for an asset-shipping toolkit and a bad one for a procedural renderer.

The second, subtler finding: libadwaita converts an accent to a colour twice, via adw_accent_color_to_rgba() and adw_accent_color_to_standalone_rgba(). A fill accent and a text accent are different colours for the same role, because the hue that reads as a background fails contrast as text. Any derivation that treats the accent as one colour gets one of those two cases wrong — which is why the derivation places chromeAccent and chromeFocused at different tones.

Resolution for Sparkles. Accept free RGB (the platforms that have a real picker deserve it), derive per-role tones, and model the accent as optional — because on three of eight platforms there is not one.

Dimension 3 — contrast: boolean, tri-state, or forced

Three incompatible things share one word:

  1. Increase contrast — push your own colors further apart. GNOME (contrast: 0|1), macOS, iOS (accessibilityContrast).
  2. A contrast level — a magnitude, not a flag. Android 14 (Standard/Medium/High via UiModeManager.getContrast()); the web additionally has prefers-contrast: less, which nothing else models.
  3. Forced colors — stop specifying colors; the system substitutes them. Windows High Contrast, surfaced on the web as forced-colors.

The third is categorically different and the most commonly misimplemented: an app that "supports high contrast" on Windows by darkening its own dark theme has misread the signal entirely.

Resolution for Sparkles. A three-step ContrastLevel (widening at the abstraction loses nothing; narrowing loses the middle step permanently), plus a separate forced-colors flag that suppresses decorative color rather than scaling it.

Dimension 4 — who builds the palette

Platform shipsPlatformsWhat the app must do
A complete semantic paletteiOS, macOSname roles; write no color code
A generated tonal paletteAndroidname tokens; the wallpaper drives the rest
A full role-scoped paletteKDEmap its roles onto yours
Nothing but scalarsGNOME, Windows, web, terminalderive everything

This is the sharpest fork in the survey, and it decides the shape of a cross-platform implementation. An app targeting only Apple platforms writes no color code; an app targeting GNOME and the terminal writes all of it. An app targeting both must have a derivation anyway — so the derivation is the baseline, and platform-supplied palettes become an optional refinement layered on top, not an alternative architecture.

Practically: derive from the triple everywhere, and let KDE (the one Linux desktop that publishes real colors) override individual slots when its scheme is readable.

Dimension 5 — change delivery

Universal push, but with three recurring defects that a consumer must assume:

  • Duplicate delivery. Observed live on GNOME: SettingChanged fired twice per change with two portal backends registered (gnome).
  • Stale-at-notification. macOS's distributed notification can arrive before the default is updated (macos).
  • Valueless notification. The terminal's CSI ? 997 n deliberately carries no value; Android's config change carries a whole Configuration you must diff.

Every one of these is neutralized by the same discipline, which the field has independently converged on and which the terminal spec authors wrote into the protocol:

A notification means "re-read". It never means "the value is now X". Compare against the value you hold, and repaint only on a real difference.

The web is the sole exception — matchMedia change events carry a boolean the UA already computed — and it is also the only platform where the app need not participate at all.

Dimension 6 — reachability from a self-rendering application

The dimension that matters most for Sparkles, since sparkles:ui paints its own pixels on every target.

PlatformCost of following the system
Terminaltwo write(2)s, a poll(2), a parser — no dependency
macOSone CFPreferencesCopyAppValue; change delivery needs a run loop
GNOME/KDEa D-Bus client (one call, one signal) — no toolkit, sandbox-transparent
KDE extraplus an INI parse for the full palette
WindowsRegGetValueW + SystemParametersInfo; accent needs WinRT
AndroidAConfiguration_getUiModeNight free; accent and contrast need JNI
iOSnothing — the host shim must push the traits in

The pattern: the scheme is cheap everywhere; the accent is the expensive part on three platforms and absent on three others. That asymmetry argues strongly for shipping scheme-following first and accent-following as a separate, capability-gated increment — which is how the proposal sequences the work.

What follows the system, and what must not

The survey's clearest cross-platform policy, stated by Apple as "preserve meaning across appearances" and implied by Material's token discipline:

Follows the systemDoes not follow
page background / default foregroundsyntax-highlighting rules (the user chose that theme)
panel and popup surfaces, borders, dividerssemantic status: error, warn, info
gutters, scrollbar track and thumbdiff colors (diffAdded / diffRemoved mean add/remove)
focused-pane band, selection tint, caretanything the user configured explicitly
chrome accents and key hintsbrand colors an app deliberately owns

libadwaita states the precedence rule for the last row exactly: "apps are still free to set their own accent color, and CSS always takes priority over the system accent". The system supplies defaults, not overrides. An explicit user or app choice always wins.


Delta table — where Sparkles stands today

Against the consensus, per capability. Sources: libs/ui/src/sparkles/ui/theme.d, libs/ui/src/sparkles/ui/style.d, and the sparkles:ui theme spec.

CapabilityConsensusSparkles todayGap
Widgets name semantic roles, not colorsuniversalSlot + PaletteTHM1 already requires it✓ none
Theme is one runtime-swappable valuerequired for change handlingTheme carries all four channels; THM7 is partial~ partial
Light/dark from the OSuniversal, push-deliveredabsent — scheme is inferred from the theme's own background via schemeForBackground
Three-valued scheme5 of 8 platformsabsentColorScheme is a two-valued enum
Accent color5 of 8 platformsabsent — no slot is accent-derived; chromeAccent is authored
Contrast level6 of 8 platforms; 3 steps on 2 of themabsent
Forced colors / high contrastWindows + webabsent
Reduced motion6 of 8 platformsabsent
Change notification → re-themeuniversalabsent — no appearance source, so nothing to notify
Per-feature capability flagslibadwaita's five has_* predicatesabsent, but the repo-wide DbI protocol is the same shape
Capability-gated degradation per targetuniversalTHM8 specifies it (not started); the warning about not gating on isTerminal is already correct~ specified
Tone-based derivationAndroid; implied by Apple and Materialabsent — palettes are authored as hexes
Contrast verified rather than assumedMaterial claims it structurallyabsent
Multi-theme HTML outputweb's light-dark() / prefers-color-schemeDEF5researched/not-started

The summary is unusually clean: Sparkles' architecture is already right — semantic slots, one theme value, resolution at display-list construction, a capability-gating requirement already written down. What is missing is entirely the input: there is no source of OS preferences, so Theme is authored rather than derived, and ColorScheme is inferred from the very theme it should be selecting.

That is a good place to be. The gap is a new library and one new channel into an existing type, not a refactor of the toolkit.

What the proposal takes from each subject

SubjectContribution to the design
libadwaitaper-feature capability flags; the backend cascade; declared degradation
GNOMEthe portal as the one Linux route; probe per key; debounce duplicate signals
KDEColors:ViewColors:Window; a platform palette can override derived slots
Terminalthe first backend to build; timeout-as-capability-detection
Androidtone-based derivation; the three-step contrast level
iOSaccent must be optional; elevation is a colour input
Windowsforced colors ≠ high contrast; stage the cheap path before WinRT
macOSread the raw default without the framework; bestMatch, not name equality
Weblight-dark() for the HTML sink; bake the accent in, it cannot be read back
color-derivationplace by tone, accept by ratio; chrome follows, content does not

The Sparkles proposal

Sources

Each claim above is carried by the deep-dive it links to; those pages hold the primary sources. The measurements are produced by the four runnable examples the ci helper compiles and runs on every pass.