Skip to content

Accessibility

The target is WCAG 2.2 Level AA for the web app, which is how everyone uses Noct in v1: setting up, signing in, browsing, playing, and looking after the server. Native apps are later, and will have their platform’s own standards (VoiceOver and Dynamic Type on Apple’s, and so on) as theirs.

This is the target, not a claim. Noct does not yet conform, and this page says what has been checked and what hasn’t, so that it can’t be mistaken for more than it is.

  • Everything can be done with a keyboard alone, in an order that makes sense, with the focus always visible and never hidden behind something (2.4.11).
  • Everything has a name a screen reader can say: links and buttons by what they do, form fields by what they’re for, pictures that mean something by what they show, and ones that don’t by nothing.
  • Pages have titles, landmarks and headings that say where you are, and a way past the navigation. Changes that happen without a page load (a toast, a scan finishing) are announced.
  • Text has 4.5:1 contrast against its background (3:1 for large text and for the parts of controls); colour is never the only way something is said, which matters here because colour is used for status.
  • It works at 200% zoom and at a 320px width without scrolling in two directions, respects reduced motion, and keeps what it shows in forced-colours modes.
  • Targets are at least 24 by 24 pixels (2.5.8), nothing needs dragging (2.5.7), and signing in doesn’t need remembering or transcribing anything the browser can’t fill in (3.3.8): password fields allow pasting and password managers.
  • Components are built on Base UI, which supplies the roles, names, focus handling and keyboard behaviour of menus, dialogs, tabs, selects, switches and so on, rather than each being made by hand.
  • Tests find things as a person does, by role and name, so a control with no accessible name can’t be tested at all.
  • An automated sweep with axe (apps/web/e2e/90-accessibility.spec.ts) runs over the pages and over a few states (a movie, a season, search, a trailer, the add-folder dialog), against the WCAG 2.2 AA rules, and fails the build on anything moderate or worse. It doesn’t yet cover every dialog and state.
  • A keyboard audit (apps/web/e2e/91-keyboard.spec.ts) tabs through every page from the top and fails on a stop whose focus can’t be seen, is covered by something, or is under 24 by 24 pixels with another target close by (2.4.11, 2.5.8), and on a page with sixty tab stops or more. It also checks that closing a dialog or menu returns the focus to what opened it, and that an open dialog doesn’t let it out.
  • A display audit (apps/web/e2e/93-display.spec.ts) goes over the same pages at 400% zoom (320 CSS pixels wide, no sideways scrolling), with the text spacing 1.4.12 asks to be allowed (no text cut off), with reduced motion asked for (nothing left moving), and in forced colours (the focus drawn as an outline, which is all that’s left there). The colour tokens are held to 4.5:1 for text and 3:1 for what is drawn (shared/styles/tokens-contrast.test.ts); one case, a danger badge on a subtle row at 4.43:1, is known and waits on the brand’s red.
  • Every component in every state it has a story for is checked with axe in Vitest on every push (src/shared/test/stories-accessibility.test.tsx). jsdom has no layout, so colour contrast is left to the sweep above.
  • Every page has a title, one h1, headings that go down a level at a time and one main with its navigation named, which the sweep checks page by page.
  • A phone-sized screen is looked at by the end-to-end suite (responsive).
  • Every page has its own title (useTitle), which is announced when the page changes without a page load (RouteAnnouncer), and a skip link is the first thing the keyboard reaches. Both layouts have one main, and their navigation and the activity panel are labelled.
  • Subtitles for the deaf and hard of hearing and audio description tracks are flagged in playback, from the file’s own markings and from sidecar names (.sdh, .hi, .cc), and an audio description is never chosen as a film’s sound on its own.

Books’ reader (/books/read/{id}, for EPUB and PDF) is a page of its own, and is in the axe sweep (e2e/90-accessibility.spec.ts) with its contents, search and settings open. What a book holds is the book’s own to make accessible, so the sweep leaves the book’s frame out; everything around it is ours:

  • One main, named for the book, a heading for its title, and a name for every control. Each has a keyboard path: arrows and Page Up and Down turn pages, t opens the contents, / the search, Escape closes what is open and then leaves. The keys work from inside the book too.
  • Bookmarks, highlights and notes: b bookmarks the page, a lists them all, by chapter, each with a button to go to it, to edit it and to delete it, named for what it is. Words are selected with a pointer, and then h and n (and buttons) do the rest; with no pointer, each result of a search has a button to highlight it and one to write a note on it.
  • The book has the focus when it opens, and gets it back when the contents, search or settings are closed, so the keys are always the reader’s.
  • A status line that only a screen reader hears says where the reader is, as the pages turn (“Chapter Two, 37% read”, “On page 12 of 300”), and a search says how many it found.
  • The three themes have text and links of at least 4.5:1 on their page (settings.test.ts). The size of the text, its font (one for dyslexia), line height and margins are the reader’s to change, and reduced motion turns page animations off.
  • A PDF’s page is a picture of it with its words over it, so what is read out and selected is the text and not the picture, which is hidden from a screen reader.

Checklist for a release, with VoiceOver (macOS, Safari) and NVDA (Windows, Firefox)

Section titled “Checklist for a release, with VoiceOver (macOS, Safari) and NVDA (Windows, Firefox)”

By hand, with an EPUB and a PDF, since automated checks find perhaps a third of what can go wrong:

  • Opening a book says its title, and the focus is on the book.
  • The book’s text is read from where the reader was, by line and by paragraph.
  • Turning a page, by key and by button, is told: the chapter and how far, or the page.
  • The contents and the search are reached by key, are announced as dialogs with their names, read each entry in full (the chapter and the words around a result), and give the focus back.
  • The count of results is heard when a search finishes, and “No results” when there are none.
  • The settings are reached, each says what it is and what it is set to, and a change is heard or seen at once.
  • Escape from a dialog does not leave the book; Escape from the book does.
  • At 200% zoom and at phone width, nothing is cut off and nothing needs a mouse.

Automated checks find perhaps a third of what can go wrong. These are tracked, and until they’re done Noct shouldn’t be called conformant:

  • Axe on every page and state, with the WCAG 2.2 AA rules (#119)
  • One h1 on every page with no skipped heading levels, and page changes confirmed as heard with VoiceOver and NVDA (#120)
  • A keyboard and focus audit of every flow (#121): pages and the dialogs’ focus are done; the flows themselves (players, editing, the reader’s own keys) and anything that needs hover or dragging are not
  • A pass with VoiceOver and NVDA, and a checklist for each release (#122; the reader has its own, above)
  • Zoom, reflow, reduced motion, forced colours and contrast (#123): the pages are checked as above; the players, dialogs and the reader at 400% and in forced colours are not, and neither is whether status is ever shown by colour alone (1.4.1)
  • The tray app, which is a separate surface (#125)

A new screen or flow is added to the axe sweep, can be used with the keyboard alone, and has names for everything on it, before it is finished. A bug someone meets with a keyboard or a screen reader is fixed like any other, with a test at the lowest level that can show it.

If something can’t be used with the way you use a computer, that’s a bug: open an issue and say what you were trying to do and what you use. You don’t need to know what rule it breaks.