Accessibility That Fails the Build
National flag colours are often too light to read as text. afrigov checks every colour pair at build time, derives a readable shade when an official colour fails, and fails the build when anything still doesn't pass.
contents (6)
Accessibility guidelines are easy to agree with and easy to break. A designer picks a colour that looks fine on their screen, a developer copies it, and six months later the site fails an audit nobody ran.
In afrigov, the design system I build for African public-service websites, the rules are not guidelines. They are build steps. If a colour combination is unreadable, the build fails. If the CSS gets too heavy, the build fails. This post covers how that works, starting with the hardest case: national colours.
The problem with flag colours
Each country in afrigov is a pack: one JSON file declaring its official colours, flag stripes and the wording of the official-website banner. Here is part of Kenya’s:
{ "official": { "$type": "color", "black": { "$value": "#000000" }, "red": { "$value": "#bb0000" }, "green": { "$value": "#006600" } }, "color": { "$type": "color", "primary": { "$value": "{official.green}" }, "accent": { "$value": "{official.red}" } }}Government sites want their national colours, and they should have them. But official colours were chosen for flags, not for text on a white page. Many are too light to read. The WCAG standard asks for a contrast ratio of at least 4.5:1 for normal text, and a bright flag yellow or light green can fall well short.
The usual outcomes are bad either way. Use the official colour for links and buttons, and some readers cannot read them. Swap it for a darker shade by hand, and every site does it differently, if at all.
Derive the readable colour, keep the official one
The build takes a different approach. A pack only declares its official colours and which one is primary. The build then does the rest:
- If the primary colour is too light to meet 4.5:1 as text on the page background, it is darkened step by step until it does.
- The change is written into the generated CSS as a comment, saying which official colour was adjusted and why. Nothing is changed silently.
- The flag stripe always uses the exact official colour. It is decoration, not text, so it never needs adjusting.
- The accent colour is only used as a background or decoration, never as text, so it keeps its exact official value too.
From the primary colour the build also derives the hover state, the text colour on buttons, a light tint, and link colours, so a pack author never picks those by hand.
The result is that a country gets its real colours where they identify the site, and readable versions of them wherever someone has to read.
Every pair is checked, for every country
Deriving a colour is only half of it. The build then checks around two dozen foreground and background pairs, for the core and for every country pack:
| Pair | Why |
|---|---|
| Body text on the page | Everything people read |
| Hint text on the page | Labels under form fields |
| Button labels on the primary colour | Every call to action |
| Links, and visited links, on both backgrounds | Navigation |
| Success, warning, error and info text on their tints | Alerts |
| Body text inside each kind of alert | Links and text in a warning box |
| Text on the accent band, and the selected menu item | Highlighted sections |
Each pair must meet its ratio. If one fails, the build stops. Adding a new country means copying a pack, changing the values and running the tests. If the colours do not work, the contributor finds out before the pull request, not after a ministry publishes the site.
More rules that live in CI
Contrast is the clearest example, but not the only one.
A size budget. The core CSS must compress to 20 KB or less, or the build fails. It is currently under 10 KB. Readers pay for every page from their phone data, often on a prepaid bundle, so page weight is an accessibility issue, not just a performance one.
axe-core on every page, in every theme. The docs site renders every component and is tested against WCAG 2.1 A and AA rules with the core and with each of the seven country packs. That is 720 automated accessibility runs on every pull request.
Touch targets. Every link, button and control is checked for a minimum height at phone width, because a tap target that is too small is a real barrier for many people.
Version consistency. A test fails if the README or templates point to any version other than the current one, so documentation cannot quietly drift out of date.
Rules that are designed in, not tested
Some decisions are not checks at all. They are choices that remove whole classes of problems:
- System fonts only. Zero font downloads. Noto Sans is named early in the font list because it covers the characters Yoruba, Hausa, Igbo, Wolof and Fula need.
- Body text at 16px with generous line height, so accents and diacritics never collide between lines.
- Links are always underlined. Colour alone is never the only signal.
- No date pickers. Three plain fields for day, month and year work on every phone and every screen reader.
- Every component works without JavaScript. The optional script only adds collapsible navigation and focus handling.
Why build it this way
Accessibility work usually happens at the end, as an audit, after the decisions are made. By then, fixing a colour means a redesign and a meeting.
Putting the rules in the build moves that moment to the start. A contributor who adds an unreadable colour finds out in seconds, from a failing test that says which pair failed and why. Nobody has to remember the rules, because the build remembers them.
You can see the results on the use cases site, where real government pages rebuilt on afrigov score 100 on afrigov-audit.