Accessibility
OnlyTons is built to WCAG 2.2 Level AA. This page states the standard we hold ourselves to, how we test against it, what we know is still wrong, and how to tell us when we have missed something.
If you cannot use part of OnlyTons, that is a defect and we want to hear about it. See Telling us about a problem at the bottom — you do not need to know which rule was broken.
The standard, and why this one
WCAG 2.2, Level AA. Not "we try to be accessible" — a named, published, testable standard with numbered success criteria.
We chose 2.2 AA for three reasons:
- It is what EN 301 549 references — the European standard for accessibility requirements in public procurement.
- The European Accessibility Act has applied to e-commerce services since June 2025. OnlyTons is a B2B trade platform operating in the EU, so this is likely a legal obligation and not a courtesy.
- It is the version that added the criteria this product most needed: 2.5.8 Target Size, 2.4.11 Focus Not Obscured and 3.3.8 Accessible Authentication.
Whether the Act applies to OnlyTons specifically is a question for counsel, and it is recorded as such in our open-questions register. We build to AA either way, so the answer changes the paperwork, not the product.
How we test
Three layers, and none of them is sufficient alone. Roughly a third of WCAG can be checked by a machine; the rest needs a person.
| Layer | What it catches | When it runs |
|---|---|---|
Lint (eslint-plugin-jsx-a11y, every rule an error) | mistakes visible in the source: a label pointing at nothing, a clickable <div>, a missing alt | on every change |
Automated audit (pnpm test:a11y — Playwright + axe-core) | mistakes visible in the rendered page: colour contrast, unnamed controls, heading structure, landmark duplication | on every change, over every route, as an anonymous visitor, as an operator, and as the broker |
| Manual pass | everything else: whether the keyboard order makes sense, whether a screen reader announcement is useful rather than merely present, whether a page is usable at 200% zoom | before each release, and whenever a screen changes substantially |
The automated audit checks the WCAG 2.0, 2.1 and 2.2 A and AA rule sets plus axe's best-practice rules, in the light theme for every route and in the dark theme for the four screens where colour carries meaning.
Coverage is enforced, not assumed. The list of routes the audit visits is the same list the application's routing is checked against, so a new screen cannot be added without either being audited or being explicitly excluded with a written reason. There is no way to add a page and quietly leave it untested.
This manual is audited too. These pages are part of the platform, at
/docs, so they are in the same sweep — and four things that would otherwise be
invisible in a text file are checked before a browser ever sees them: no skipped
heading level, no table column left without a header, and no internal link or
section reference that leads nowhere. The first run of that check found five
tables in each language whose first column had no header, which is exactly the
kind of thing that reads fine and announces badly.
What we have done
Specific things, so you can check them rather than take our word for it.
Getting around
- Every page starts with a "Skip to main content" link. It is invisible until you Tab to it, then it is a normal, visible button. It exists because the sidebar is fourteen links long and you should not have to walk it on every navigation.
- Every page has one
<h1>naming it, so you can navigate by headings. - Landmarks are unique and named: one navigation region, one main region, and where a page has a filter rail or a sheet, that region has its own name.
- The mobile menu closes with Escape, its button says whether it is open, and when it is closed it is genuinely gone — not merely pushed off-screen where a keyboard could still reach it.
Forms and controls
- Every input, select, checkbox and radio has a label. Where a control has no visible label — a filter, a unit picker beside a quantity box — it carries a spoken one.
- Choices from a fixed set are real radio groups and checkboxes, so arrow keys work and the selected option is announced. The dispute form on a contract used to be a row of buttons that looked selected without saying so; it is a radio group now.
- Sortable table columns announce which column is sorted and in which direction — and the direction stated is the direction actually used.
- Buttons that are only an icon carry a spoken name. "Edit chickpeas", not "button".
Colour and motion
- Nothing depends on colour alone. The conditions panel on a contract carries an icon and a spoken word for every outcome; the map's toggle says which view is active rather than only shading it.
- Contrast meets AA. The destructive-action button was measured at 3.99:1 resting and 3.31:1 on hover — it got harder to read as you pointed at it — and now measures 6.18:1 and 5.13:1.
- If your system asks for reduced motion, every animation and transition in OnlyTons is suppressed. The automated audit itself runs with reduced motion on, so that path is the tested one.
The map
The land map is a genuinely hard case, and our answer is that the map is never the only way to get the information.
- The table is the default view, not a fallback. It renders the same data from the same request as the map, so the two cannot disagree.
- A skip link above the map goes straight to the table.
- Map markers are focusable in document order, each with a spoken name carrying its coarse area, and each draws a visible focus ring.
- Markers are drawn as circles, not pins, because a parcel's position is deliberately approximate (see Operator privacy) and a sharp dot would claim a precision that does not exist.
Signing in
Password sign-in has no puzzle to solve, no image to interpret and no timed challenge — that is WCAG 2.2's 3.3.8 Accessible Authentication. The anti-abuse check runs invisibly in the background and does not ask you to do anything.
What is still wrong
Being honest about this is part of the standard, and it is more useful to you than a claim of full conformance would be.
| Gap | What it means for you | Status |
|---|---|---|
| Dragging deals between stages on the broker board is pointer-only. | Keyboard users cannot drag. | Not a blocker: the deal room's "Advance to…" button does the same thing, works with a keyboard, and is the better control anyway because it explains why when a step is refused. Keyboard dragging is on the list. |
| The registration wizard has not been driven with a screen reader. | Unknown. It is the one flow our automated audit cannot reach, because the audit signs in with already-approved accounts. | Covered by the manual pass; scripted in our QA checklist. |
| Screen-reader testing is not yet continuous. | Announcements are correct as far as the automated tools and our manual checks can tell, but there is no automated screen-reader regression test. No such tool really exists. | Manual pass before each release. |
| The map is a simplified drawing, not a full map. | You cannot pan or zoom it. | Deliberate for now. When a real basemap replaces it, everything above has to survive the change, and the table stays the default. |
We do not currently publish a formal accessibility conformance statement (a VPAT / EN 301 549 declaration). If you need one, ask.
Telling us about a problem
Contact the broker through your usual channel, or write to the address on the sign-in page. Please say:
- what you were trying to do (not what you think went wrong technically);
- the page you were on;
- what you were using — screen reader and browser, keyboard only, magnifier, voice control, whatever it was.
You will get an answer from a person. An accessibility defect is treated as a defect, on the same queue as anything else that stops you working.
Under the hood
For maintainers.
- The standard, the rule tags and the route list live in
apps/web/a11y/andapps/web/src/lib/release/routes.ts. pnpm test:a11yneeds the local Supabase stack up and the demo data seeded (pnpm docker:supabase && pnpm seed:demo). It signs in as the seeded broker and a seeded operator, because half the product is unreachable without a session, and a page rendered as an empty state is not the page.- One axe rule is disabled and the reason is written beside it in
a11y/sweep.spec.ts(Next's development overlay, which is not shipped). A disabled rule without a written reason is how an audit becomes a formality. eslint.config.mjspromotes everyjsx-a11yrecommended rule to an error. Before S12 no lint rule in the repository ran at all — the config was an ESLint 8 file under ESLint 9 — so the accessibility findings from that session are a first run, not a regression.
Italiano → accessibilita.md