Measure the palette contrast and fix what it found - #83
Merged
Conversation
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
This was referenced Aug 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The
xraywidgetsexit 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.themecan now work out a WCAG contrast ratio from two hex colours, andpyxray/tests/test_theme.pyasserts every pair issue #69 named: each tone's text against its own fill,MUTEDandINKagainst both pages, the focus outline against both pages, and the edge of a control against both pages. The thresholds aretheme.BODY_TEXTat 4.5:1 andtheme.LARGE_TEXTat 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 ondurable, 3.11:1 onwarning, 3.32:1 oninputand 3.77:1 onintermediate. Onlyquietcleared 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.textnow, it isINKfor 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.
LINEis 1.5:1 against white andDARK_LINEis 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 separateEDGEnow 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 assertsLINEstill fails that bar, which is the fact that makesEDGEworth 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.pyreads the rendered markup and the sheet back and checks the pieces are there: no styled div where a button should be,aria-pressedon everything pressable, a real<textarea>,scope="col"on every heading, a:focus-visibleoutline 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, fromstrings.pylike 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 checkis green: 1488 passed, 3 skipped, up from 1431. No diagram or animation changed, because no stroke or fill moved.xraywidgets/README.mdandxraymanim/VISUAL-SYSTEM.mdboth say what was measured and why the words on a fill are ink.Closes #69