Skip to content

Accessibility in Gextto

How the interface is kept usable with a keyboard, a screen reader, a magnifier and a phone: what is automated, what is checked by hand, and what a change has to pass before it is finished.

Target: WCAG 2.1 AA, compatible with the 2.2 practices where they do not change the interface. This is not a certification. The automated baseline passes; a formal declaration also needs the manual audit described below.

Where it applies

Interface What it is
Gextto web UI (uiweb/v2) Every SSR + HTMX page: dashboard, downloads, series, movies, comics, archive, settings, maintenance, health, logs, integrations
gx-torrent page (cmd/gx-torrent) Standalone client: torrent list, context and column menus, detail panel, RSS and Wanted editors
TUI (gexttod tui) Keyboard only by nature; the layout compacts below 60 columns for an SSH session on a phone
Manuals Markdown served in the app: headings, lists and links are read by screen readers

What is in place

  • Semantic HTML: landmarks, skip link, aria-current, headings in order, lists for lists.
  • Every control has an accessible name; icon buttons carry text or aria-label; fields have a label, not just a placeholder.
  • Dialogs: focus enters the dialog, Tab is trapped, Esc closes, focus returns to the control that opened it.
  • Keyboard: every action is reachable; sortable headers expose aria-sort; column and panel resizers expose aria-valuenow; menus are navigable; nothing works only with a mouse.
  • Live regions announce async states, errors, progress and notifications; polling does not move the focus.
  • Tables have a caption and scope; the torrent table is a role="grid" with aria-multiselectable; progress bars carry a value.
  • Contrast: colour tokens for light and dark themes, checked minimums; the 20+ themes and the custom ones use the same tokens.
  • Reflow at 320 px without horizontal scroll; reduced motion respected; zoom 200 % and 400 % supported.
  • Six languages, including the strings generated by the client JS. Labels, descriptions and tooltips are translated and checked by make test.

Automated checks

Playwright with axe-core, uiweb/end2end/tests/accessibility.spec.ts:

  • axe with the wcag2a and wcag2aa tags on /, ?view=downloads, ?view=settings, ?view=maintenance, ?view=integrations, ?view=health, ?view=logs: no violations;
  • accessible names on the search and language controls;
  • the settings search understands common synonyms;
  • dialogs keep the focus and return it on close;
  • polling does not lose the focus on the torrent control;
  • the release search works from the keyboard;
  • no horizontal scroll at 320 px;
  • scrollable log areas are keyboard reachable and have a region role.

Run the suite:

cd uiweb/end2end
npm ci
npm run test:a11y        # 14 tests

The CI job ui-e2e runs the whole Playwright suite on every push, so a regression on these tests blocks the build.

The static guards in make test are not a substitute for axe, but they prevent controls that cannot even be read: every setting is indexed, writable, translated in six languages and has a tooltip, and every button of the UI points at a registered route (TestUIIsTranslatable, TestSettingTooltipsAreTranslated, TestSettingsIndexIsWritable, TestUIActionPathsAreRegisteredRoutes).

Manual verification

Before a release, and after any UI change that touches focus, layout, contrast or async states:

  • keyboard only: Tab/Shift+Tab order, Enter/Space, arrows, Esc; every dialog, menu, table and form;
  • a desktop screen reader (NVDA + Firefox or VoiceOver + Safari) and TalkBack on a phone;
  • zoom 200 % and 400 %: nothing clipped, dialogs usable, no horizontal scroll;
  • forms with the screen reader: error summary, field association, invalid state, recovery;
  • async states not colour-only: loading, success, warning, failure, empty, retry;
  • both themes, a mobile viewport, reduced motion;
  • external content injected by the page (covers, release names) does not break the reading order.

Acceptance criteria

  • Every control has a reliable name and role.
  • Every mouse action has a keyboard equivalent.
  • Focus is contained in dialogs and restored when they close.
  • Async announcements are informative and not excessive.
  • Contrast passes in both themes.
  • Tables, tabs, progress bars and errors have usable semantics.
  • No regression on desktop or mobile.

Adding or changing a control

  1. Use the existing component (label + .input, dialog, ul.items, .chip, .badge, .fsec) and the colour tokens; never a fixed colour.
  2. Give the control a label (or aria-label/aria-labelledby) and, for an icon, a text alternative.
  3. Check it from the keyboard: what receives focus, what Tab reaches next, what Enter/Space/Esc do, where the focus returns.
  4. If a state changes asynchronously, announce it in a live region.
  5. Add the string and its tooltip to the six catalogues; make test enforces it.
  6. Run npm run test:a11y. For a new page, add it to the axe list in accessibility.spec.ts.
  7. For anything that changes interaction, do the manual check on a phone and with a screen reader.

A UI change without this check is not finished.

Audit and known limits

  • The gx-torrent pages were audited with axe-core (light and dark theme, sample torrents, feeds, rules and wanted titles) with 0 violations. The first audit found and led to fixing: resizers without a value, zero-count filters below the contrast minimum, aria-selected rows outside a grid, aria-expanded on a row, preview lists that could not take the focus, and the progress percentage drawn over the bar.
  • The automated suite is a safety net, not a certification. The manual screen-reader, zoom and assistive-technology pass is still required for a WCAG/EN 301 549 statement.
  • The TUI relies on plain text, a stable order and the terminal's own screen-reader support; it has no graphical semantics of its own.
  • The accessibility results of each round, with the machine and the date, are in measurements.md.