Skip to content

Accessibility model

This page records the reasoning behind sorta11y’s accessibility decisions, so they can be reviewed, challenged and re-tested rather than taken on faith. Measured screen-reader behaviour and the manual verification plan live in the AT test matrix.

Each grab target is a tab stop with an explicit grab / move / drop interaction.

With a drag handle (recommended) the handle is a <button> exposing aria-pressed — the richest state, and fully axe-clean.

Without one, the <li> stays a native listitem and the grab state is announced via the live region instead. This is deliberate: role="button" is not valid on an <li>, and aria-pressed requires role="button". Rather than break the list semantics to get a state attribute, sorta11y keeps the list a list and moves the state into speech.

Intentionally no aria-grabbed / aria-dropeffect — both are deprecated and unreliably supported.

Screen-reader focus mode (role="application")

Section titled “Screen-reader focus mode (role="application")”

NVDA and JAWS swallow Space and the arrow keys in their default browse mode, so a custom widget’s keys look dead to exactly the users who most need them to work.

sorta11y follows the pattern GitHub uses for its own sortable lists, and MDN’s “scope it as small as possible, last resort” guidance for role="application":

  1. The list is wrapped in a role-less <div class="s11y-app">.
  2. role="application" is toggled onto that wrapper only for the duration of a grab.
  3. It is removed again on drop or cancel.

So the reader enters focus mode exactly when the arrow keys are needed, and the idle list stays a fully readable list the rest of the time.

Pickup is initiated by the handle button’s activation — a click, which survives browse mode — rather than by a raw Space keydown. This is why a real <button> handle is recommended for full screen-reader support.

Engaging the role is not enough on its own: NVDA and JAWS re-evaluate browse vs focus mode only when focus moves, and at pickup focus already sits on the grab target. So pickup moves focus onto the list itself (via a temporary tabindex="-1", removed again afterwards; a tabindex you set yourself is left alone), inside the freshly engaged application region. Focus returns to the grab target as the item moves, and on drop or cancel. Blurring and refocusing the same element does not work — browsers coalesce it before it reaches the screen reader. The measurements behind this are in the AT test matrix.

Opt out with applicationRole: false (or data-application-role="false"). ARIA requires application regions to be named; the wrapper mirrors the list’s own name — its aria-labelledby (unless it references no existing element), else its aria-label — falling back to the applicationLabel string (“Sortable list”) when the list has neither.

Each instance owns one live region, aria-live="polite" by default and configurable via the liveness option. It is inserted when the list is enhanced, not when the first announcement happens — a region created and filled in the same tick is frequently missed by screen readers.

The live region and the hidden instructions sit next to the .s11y-app wrapper, not inside it: live regions nested in an active application region can be announced inconsistently by NVDA and JAWS.

Every step a user takes goes through it: keyboard pickups, moves, drops and cancels, tap pickups and placements, and the end of every pointer drag, dropped or cancelled. A user-driven reorder is never silent. A programmatic sort() is the app’s own change and announces nothing.

The same grab / move / drop model is offered to a single pointer, including a tap-to-pick-up alternative to dragging (WCAG 2.5.7). The full reasoning, and the clickToGrab / dragOnItem / dragOnItemTouch trade-offs, are in Pointer & touch.

  • It does not claim conformance that has not been measured. The three official AT combinations are still untested; only an informal NVDA + Chrome run has passed — see Browser & AT support.
  • It does not support nested lists, transfer between lists, or grid reordering. Those need a different interaction model to be accessible, not a wider version of this one.