Study protocol

To compare system colors across browsers, we need observations taken in the same way. This is how we collect them, what the measurements tell us, and where we still need to test for ourselves.

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:

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

  1. 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.
  2. 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.
  3. Check the device clock. The snapshot’s timestamp comes from that clock and is saved in UTC.
  4. 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.
  5. If forced colors are active, enter the name of the Contrast theme or custom browser palette. Note any changes you made to its colors.
  6. 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.
  7. 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.
  8. 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.
  9. Submit one checked snapshot through the linked GitHub issue form. The workflow validates the data and the combined build. I can then comment /approve-snapshot to 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:

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:

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.