Skip to content

Measure the palette contrast and fix what it found - #83

Merged
tamnd merged 1 commit into
mainfrom
contrast-measured
Aug 29, 2026
Merged

Measure the palette contrast and fix what it found#83
tamnd merged 1 commit into
mainfrom
contrast-measured

Conversation

@tamnd

@tamnd tamnd commented Aug 29, 2026

Copy link
Copy Markdown
Owner

The xraywidgets exit criterion on #14 asks for adequate contrast in both themes. #68 built the widgets so that it should hold. Nobody had computed a number, and this turns "should hold" into a test that fails when it stops holding.

What is new

pyxray.theme can now work out a WCAG contrast ratio from two hex colours, and pyxray/tests/test_theme.py asserts every pair issue #69 named: each tone's text against its own fill, MUTED and INK against both pages, the focus outline against both pages, and the edge of a control against both pages. The thresholds are theme.BODY_TEXT at 4.5:1 and theme.LARGE_TEXT at 3:1, and they live next to the palette rather than in the test, so anything else that wants to check a colour is checking against the same bar.

Every failure message prints the ratio and both colours. Somebody who picks a prettier blue and breaks this wants to know how far off they are.

What it found

Two real problems, both fixed here.

Words on a chip were the tone's own stroke. Measured, that is 2.67:1 on focus, 3.00:1 on durable, 3.11:1 on warning, 3.32:1 on input and 3.77:1 on intermediate. Only quiet cleared 4.5:1. The obvious fix is a darker stroke, and it is the wrong one: getting a stroke to 4.5:1 against its own pale fill means a stroke close to black, and then every line in every diagram is the same near black and the tones stop telling each other apart on white.

So there is a Tone.text now, it is INK for every tone, and it measures between 10:1 and 14:1 on all six. This is not a new idea in this repository, it is what the diagrams have always done, because Excalidraw's bound text takes the default stroke colour. The widgets were the ones quietly disagreeing. The tone still carries meaning through the fill and the border, which is two of the three channels.

There is a test that asserts the old arrangement fails, naming the five tones. It is the evidence for the decision, so it does not get re argued from scratch in six months.

The edge of a button was the rule colour. LINE is 1.5:1 against white and DARK_LINE is 2.11:1 against the dark page. That is right for a rule under a column heading, which is decoration, and nowhere near the 3:1 that WCAG asks for the boundary of a control, which is the only thing saying there is a button there. There is a separate EDGE now at 3.32:1 against white and 5.19:1 against the dark page, so one grey does both themes and a control keeps the same edge either way. A test asserts LINE still fails that bar, which is the fact that makes EDGE worth having as a separate value.

Two things that were already right, now nailed down

The dark neutrals were four hex strings written into style.py. They are in the theme now, next to the light ones, so the contrast test can reach them and so there is one place to look when somebody asks what colour this project's dark page is.

Anything sitting on a tone fill is written into the sheet as a literal colour rather than as var(--xw-ink). That was already true and was a coincidence away from not being. The fills do not move between themes and the variable does, so a variable there puts pale grey text on a pale blue chip the moment somebody's system goes dark, which is the sort of thing a screenshot taken on a light machine never shows you. There is a test.

The keyboard half

xraywidgets/tests/test_xraywidgets_access.py reads the rendered markup and the sheet back and checks the pieces are there: no styled div where a button should be, aria-pressed on everything pressable, a real <textarea>, scope="col" on every heading, a :focus-visible outline with an offset on every control, nothing anywhere that removes an outline, the front end restoring focus after a redraw for a toggle and not only for the caret, and Shift and Tab not being swallowed by the source box.

Writing those found four things missing. Both role="group" containers had no accessible name, which is worse than no group at all because it is announced as the word group and nothing else, and the bytecode table had no name, which matters in a notebook with several tables in it. All four have names now, from strings.py like every other word in the package.

None of this says the widgets work from a keyboard. It says the pieces that would make them work are present. Driving them in a real browser with a real screen reader needs a person, and that is #82 rather than being counted as done because the markup looked right. The test module says so in its own docstring.

Checks

just check is green: 1488 passed, 3 skipped, up from 1431. No diagram or animation changed, because no stroke or fill moved. xraywidgets/README.md and xraymanim/VISUAL-SYSTEM.md both say what was measured and why the words on a fill are ink.

Closes #69

The widget exit criterion asked for adequate contrast in both themes, and
nobody had ever computed a number. pyxray.theme now knows how, and a test
asserts every pair a reader looks at.

Two of them were failing. Words on a chip were the tone's own stroke, which
is between 2.7:1 and 3.8:1 against its own fill on five of the six tones
where text needs 4.5:1, so they are theme.INK now. Darkening the strokes
instead would have left the diagrams drawing six lines you cannot tell apart.
The edge of a button was the rule colour, which is 1.5:1 against white, so
there is a separate EDGE that clears 3:1 against both pages.

The dark neutrals move from style.py into the theme, so there is one place to
look when somebody asks what colour this project's dark page is.

Also a test file for the markup a keyboard and a screen reader get, which
found three groups with no accessible name and a table with no name.

Closes #69
@tamnd
tamnd merged commit 7e37c49 into main Aug 29, 2026
10 checks passed
@tamnd
tamnd deleted the contrast-measured branch August 29, 2026 04:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Measure the widget contrast ratios and drive one from the keyboard end to end

1 participant