Recursive AI

Check UI contrast

All onegame recipes

Check a game UI theme against WCAG 2.x AA contrast from the command line or a test, read which token pairs fail, and pick an accent that passes.

The component kit is made of tokens: --kit-bg, --kit-panel, --kit-text, --kit-accent and so on. KitTheme.Create builds the kit’s sheet from them and moves any text color that would fall short of AA just far enough toward white or black. Colors that aren’t text, such as your accent against the panels, it only checks. onegame lint-theme runs the game headless and reports every pair.

A failing accent

The market stall from Build a game screen with the UI kit, with a darker green accent:

C#
_kit.SetTheme(KitTheme.Create(KitTokens.Dark with { Accent = Color.Hex("#2F6B4F"), Radius = Length.Ui(6) }));
Terminal
onegame lint-theme

The output is one JSON object, worst failure first (cut here after the failures):

Text
{"passed":false,"checked":45,"failures":[{"name":"accent fill on panel","foreground":"--kit-accent","background":"--kit-panel","foregroundColor":"#2f6b4f","backgroundColor":"#1e2128","ratio":2.56,"required":3,"passed":false,"kind":"indicator"},{"name":"accent fill on field","foreground":"--kit-accent","background":"--kit-field","foregroundColor":"#2f6b4f","backgroundColor":"#171a20","ratio":2.77,"required":3,"passed":false,"kind":"indicator"},{"name":"accent fill on bg","foreground":"--kit-accent","background":"--kit-bg","foregroundColor":"#2f6b4f","backgroundColor":"#14161a","ratio":2.88,"required":3,"passed":false,"kind":"indicator"}],"checks":[...]}

It exits 1. The white text on the button passes (6.29:1), but the accent fill itself is a control indicator, which needs 3:1 against the surfaces around it, and this green reaches only 2.56:1 on the panel.

An accent that passes

A lighter green fixes the fill, but too light and the white button text fails instead. With #3F8F69 the lint reports primary button text on accent at 3.93:1, under the 4.5:1 that text needs. #367D5B passes both:

C#
_kit.SetTheme(KitTheme.Create(KitTokens.Dark with { Accent = Color.Hex("#367D5B"), Radius = Length.Ui(6) }));
Text
{"passed":true,"checked":45,"failures":[],"checks":[...]}

The pairs that moved: white text on the accent 4.95:1 (needs 4.5), the accent fill on the panel 3.25:1 and on the background 3.66:1 (need 3). onegame lint-theme exits 0, so it can gate CI.

The same check from C#

C#
var report = ThemeLint.Check(ui);             // the tokens in effect on the root; ThemeLint.Check(KitTokens) checks a token set before you use it
if (!report.Passed)
{
    foreach (var f in report.Failures)         // worst first, e.g. "primary button text on accent: --kit-accent-text #d9a441 on --kit-accent, 1.0:1, needs 4.5"
    {
        Console.WriteLine($"{f.Name}: {f.Foreground} {f.ForegroundColor} on {f.Background} {f.BackgroundColor} = {f.Ratio}:1, needs {f.Required}");
    }
}

This snippet is from the UI guide. Live control has the same check as the theme_lint tool.

Good to know

  • The ratios checked: text 4.5:1, including text on a switched-on toggle; focus ring, control outlines, accent fills and scroll thumbs 3:1; disabled text 3:1.
  • A token you override by hand after the kit’s sheet skips the repair, which is exactly what the lint is for.
  • States differ by more than color: hover and pressed change the fill, focus adds a ring outside the control, and disabled controls are flat and dim in every variant.
  • The lint checks the theme’s tokens, not every pixel of your own art. Look at a capture as well: see Screenshot tests.