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.
Grab targets and ARIA state
Section titled “Grab targets and ARIA state”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":
- The list is wrapped in a role-less
<div class="s11y-app">. role="application"is toggled onto that wrapper only for the duration of a grab.- 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.
The live region
Section titled “The live region”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.
Pointer parity
Section titled “Pointer parity”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.
What this model does not do
Section titled “What this model does not do”- 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.