[Deep Dive #13] Accessibility Railings - discussion
Accessibility work is often treated as "add aria labels at the end", but the idea of railings-as-contracts feels more like "make the UI prove it did the right thing".
What accessibility railings do you standardize in the shell (focus schedules, modal routines, skip links, announcement posture)? Do you render a11y signals (focus target, active modal, last announcement) to make issues reproducible from screenshots/tests? And how do you keep accessibility routines from turning into one-off fixes per component?
Comments (10)
Back to latestWhat does an a11y signal look like without cluttering the UI?
Is it visible text or data attributes?
For us it's usually data attributes in the shell or a tiny signals bar not visible to end users.
The goal is "test/screenshot can see it", not "user reads it".
Ok, that makes it feel way more practical. I was picturing a visible debug panel.
Our biggest win was treating focus as a routine owned by the shell, not a bunch of ref.current.focus() calls sprinkled everywhere.
Once it was a routine, we could render signals and write route-flow tests for it.
We basically used Accessibility Best Practices as the checklist for what to standardize.
Focus schedule example from the post:
tsx
import { useFocusSchedule } from "react";
export function RouteFocusRailing() {
useFocusSchedule("#main", { delay: 120 });
return null;
}
The motion can be whatever your app uses, but the key is the signal is stable and testable.
Counterpoint: a lot of a11y issues are just incorrect semantics, not focus routines.
I'm worried teams will build a signal system and still ship unlabeled buttons.
True, but railings help with the hard-to-repro issues (focus traps, modal stacking, announcements).
Semantics are table stakes; railings are the contract for behavior.
Modal stacking as route truth was the biggest a11y improvement we made. Back/forward started doing the right thing and focus bugs dropped immediately.
We rendered a stable announcement signal (lastAnnounce) and it made SR-only bugs testable.
Before that, we'd ship regressions because no one could prove what was announced without manual testing.
We tied it into our test strategy based on Testing and Debugging.
I also like the idea of a11y guardrails during migrations: if a component changes, signals can catch focus behavior drift in snapshots.
It turns accessibility regressions into observable contract changes, not vague reports.
If the UI can't explain where focus went and why, you're going to have "works for me" a11y bugs forever.
Signals also help product understand constraints. "We can't open two modals" is more persuasive when the app can show modalStack=[...].
Keep it boring: a few routines (focus, modal, announce) and stable signals. Too many railings becomes its own complexity layer.