What we’re trying to learn
A system color’s name tells us its job, but not the color a particular browser will give us. Before we choose these colors for a component, it helps to know:
- Which keywords does the browser accept?
- What values does it return for requested light and dark schemes?
- Are any of those values translucent, shared by several names, or fixed rather than drawn from the OS?
- Which foreground and background pairs meet contrast thresholds on a known surface?
- What changes with the OS, accent color, contrast settings, browsing context, browser version, or time?
These are separate questions. Accepting a keyword doesn’t prove that it follows the OS palette, and a passing contrast ratio doesn’t prove that a whole interface is accessible.
Take a snapshot we can compare
- Open the capture page in the browser and context you want to record. Use the same protocol and catalog version as the observations you’re comparing, and check for startup errors.
- Check the browser and OS versions against their About screens. Starting values come from the user-agent string, which may contain frozen or shortened versions. Where available, “Populate from browser” requests Client Hints for more detail. Neither source proves the installed versions. Correct the inferences and remove “verify” before capturing.
- Check the device clock. The snapshot’s timestamp comes from that clock and is saved in UTC.
- Open “Optional context and notes” to check the browser’s reported forced-colors state and contrast preference. Choose the actual browsing context, and note relevant device, accent, display, or experimental settings. Don’t include personal information.
- If forced colors are active, enter the name of the Contrast theme or custom browser palette. Note any changes you made to its colors.
-
Select “Capture light and dark system colors.” Each snapshot records
both requested CSS schemes and all 42 keywords against resolved
Canvas. It doesn’t change the OS settings. Don’t change settings while it’s measuring: both schemes share one environment record, not two independently checked OS states. - Download the JSON immediately. Check for unsupported keywords and transparency, and keep the original export unchanged. The page keeps only the latest snapshot in the tab; closing or reloading it loses anything you haven’t saved.
- If a result surprises you, reload and take another snapshot under the same settings. Keep both timestamps. A screenshot can help explain what you saw, but it isn’t the numeric color measurement.
-
Submit one checked snapshot through the linked GitHub issue form.
The workflow validates the data and the combined build. I can then
comment
/approve-snapshotto revalidate it and open a data-only review PR. Editing the issue requires a new approval before the PR updates. Nothing approves or merges that PR automatically.
Choose useful comparisons
Start with the Android combination that prompted a question, then broaden the comparison. Run the actual browsers where you can:
- Android
- Chrome, Android System WebView, Firefox, and Samsung Internet.
- iOS and iPadOS
- Safari and other browsers people use there. Record the actual engine and whether you’re in a browser or an embedded view.
- macOS
- Safari, Chrome or Chromium, and Firefox.
- Windows
- Edge, Chrome, and Firefox, with ordinary settings and with Windows Contrast themes.
- Linux
- Chromium and Firefox. Record the desktop environment and theme.
Collect an ordinary baseline first, then change one setting at a time. Useful comparisons include the OS’s light and dark preferences, a different accent color, an increased-contrast preference, private browsing, or an installed or embedded context. Each snapshot still records both requested CSS schemes.
Keep browser defaults separate from experimental flags and accessibility overrides. Describe what changed rather than relabeling an earlier snapshot.
Chrome’s mobile emulation isn’t an Android observation. A bundled Playwright WebKit build isn’t installed Safari or an iOS device. To record a WebView, open the page in an actual embedding app; a browser tab won’t stand in for it. An installed-app observation needs an actual installation and hosting that supports it.
The local npm run capture:local helper records the host
OS, bundled engine versions, headed or headless mode, and source
provenance. Those baselines are useful, but keep them distinct from
installed-browser and actual-device observations.
Record forced colors separately
In Windows 11, choose a theme under Settings > Accessibility >
Contrast themes. Windows 10 calls this High contrast under Ease of
Access. Return to the capture page and confirm that the browser
reports
forced-colors: active; don’t infer it from appearance
alone.
Record the theme’s name, keep Edge, Chrome, and Firefox observations separate, and take another snapshot with the theme off for a baseline. The light and dark requests may produce the same forced palette. That’s a finding, not a failed capture.
Forced colors aren’t limited to Windows. For example, Firefox’s color overrides can activate them on other systems. An increased-contrast preference and a forced palette aren’t interchangeable; record what the browser reports as well as what you changed.
New active captures require
environment.forcedColorTheme. Older files may lack a
name; that doesn’t make their active or unknown media state “none.”
When using the local recorder, pass
--forced-color-theme "Actual theme name".
Which colors and pairs we record
The catalog contains 19 current and 23 deprecated system-color keywords. Proprietary vendor keywords aren’t part of this catalog. The 10 current pairs are:
CanvasTextonCanvasLinkTextonCanvasVisitedTextonCanvasActiveTextonCanvasButtonTextonButtonFaceFieldTextonFieldHighlightTextonHighlightSelectedItemTextonSelectedItemAccentColorTextonAccentColorMarkTextonMark
We also retain five historical pairs:
CaptionText on ActiveCaption,
InactiveCaptionText on InactiveCaption,
InfoText on InfoBackground,
MenuText on Menu, and
WindowText on Window. We identify them as
deprecated; their presence isn’t a recommendation to use them in a new
design.
VisitedText here is an explicit system keyword, not a
test of someone’s browsing history. These measurements also don’t
establish what a native button, input, visited link, or
::selection will paint. Those surfaces need their own
tests.
How the measurements work
The sampler tests each keyword in the CSS color property
and reads computed styles from generic spans in separate containers
for each requested scheme. An unsupported keyword stays unsupported:
we don’t substitute an inherited color or a named approximation to
fill a gap.
We keep the original CSS strings alongside the converted sRGB bytes, hex values, and alpha. For contrast, we paint the background over the recorded opaque backdrop, then the foreground over that background. The README’s color and contrast method describes the conversion and calculation.
The capture page fixes the backdrop to resolved Canvas.
To investigate another surface, use the recorded RGBA values and
compositing method for that surface. Don’t rename a Canvas capture as
a white- or black-backdrop measurement.
Reference swatches can turn off forced paint adjustment to show the recorded values on their declared backdrop. The rest of the page still follows the visitor’s color preferences; we don’t disable them globally.
Keep aliases and fixed colors. Different names needn’t produce different values, and a system keyword needn’t expose the OS accent. If both requested schemes resolve to the same forced palette, keep both raw observations and their media metadata.
The comparison tables show both schemes. They share display columns for a forced palette only when every keyword record, backdrop, and scheme-support flag matches. Identical repeat measurements can also share columns, but every run keeps its metadata. A different measurement gets a separate column.
Check the tool as well as the data
Before widening the collection, check that the measurement and contribution paths still work:
- Run unit and browser tests, including the 21:1 black-on-white reference and unrounded contrast boundaries.
- Check keyboard operation, visible errors, metadata validation, copy and download fallbacks, table scrolling, and 200% zoom.
- Confirm an unsupported-keyword fixture produces N/A, not an inherited default.
- Confirm malformed issue data and unapproved submissions can’t create a PR.
- Inspect translucent examples against different backing surfaces.
- Check native table headers and single-row pair examples with several captures and both schemes.
- Try the download, copy, and issue flow on a real phone, including finding its saved file.
Automated checks and calculated ratios don’t establish conformance for the whole tool or a component. Screen-reader testing and review under actual OS Contrast themes are separate work.
Keep enough context to compare later
Archive the downloaded JSON with the protocol source, browser and OS
notes, and any corroborating screenshots. A dated folder under
captures/ is one option. The exports are the durable
records; the capture page has no backend or automatic browser storage.
Compare equivalent conditions before attributing a difference to a browser update. Keep versions, dates, native preferences, context, and backdrop with every observation. Repeat unusual results, and distinguish a platform change from a change to measurement or conversion.
The build combines reviewed JSON under data/ with the
root published.json, then writes the tables and details
into the results HTML. The homepage doesn’t fetch or render the
dataset at runtime, and the capture page doesn’t load it.
Both the combined build input and the generated collection must stay within 5 MB, with at most 100 unique captures. These limits bound the work on smaller devices. Keep a full archive and choose a useful published subset rather than silently dropping history. Exceeding a limit fails validation.
Review Notes and provenance before publishing. The page doesn’t send captured data to its host automatically, though ordinary page requests are still subject to that host’s access-log policy. Once reviewed and merged, a contribution is published through GitHub Pages, with the repository configured to use GitHub Actions.
Use the findings in a real interface
Choose tokens for their semantic roles, not because you like the color one machine happens to return. Look for reliable pairs and alpha-sensitive contexts, then test them on the component’s actual text and non-text surfaces.
Where needed, provide compatible fallbacks and stable, opaque backing surfaces. Preserve user-selected forced palettes, and give states indicators that don’t depend on color alone.
Use this evidence to make simpler defaults, not to add OS sniffing or production-time JavaScript contrast repairs. Document where authors need to supply paired theme colors, and keep implementations, duplicated styles, manifests, documentation, and demos aligned.
References and version
The measurement protocol and catalog remain version 1, assembled on November 5, 2026. Rewriting this guide doesn’t change the measurement method or the exported versions.
MDN’s system-color reference defines the keywords. Its general support badge and browser compatibility entries aren’t a complete browser, OS, and RGBA matrix. This study adds observations; it doesn’t claim a separately documented introduction version for every keyword. The README sources link to the specifications behind the method.