The short version
Drupal's utility for hiding content from sight while keeping it for screen readers should adopt a small set of proven improvements. The changes make screen readers announce hidden text correctly, let keyboard users see content that receives focus, and simplify the "skip to main content" link. Each recommendation below is backed by testing on real screen readers, not by inspecting the code.
If you only read one thing: two changes are proven and ready (skip-link simplification, and generated spaces that stop screen readers running words together), one is already in core and should stay (revealing hidden wrappers on focus), and three are deliberate judgment calls left for follow-up. Details below.
See it and hear it: one sample, three screen readers
The clearest way to understand the decision is one concrete case. A link reads "Place block" on screen, with the words "in the Header region" hidden but available to screen readers. What does a screen reader actually say?
Current approach (no generated spaces)
- NVDA + Chrome speaks: Place blockin the Header region — the words run together. Wrong.
- NVDA + Firefox: fine (Firefox keeps them separate).
- VoiceOver (Mac): fine.
Proposed approach (adds an invisible space at the boundary)
- NVDA + Chrome speaks: Place block in the Header region — correct.
- NVDA + Firefox: fine (unchanged).
- VoiceOver (Mac): fine (unchanged).
The proposed version is never worse and is sometimes clearly better, on one of the most common screen-reader-and-browser combinations in the world. That is the pattern for the whole decision: fix a real problem for some users, change nothing for the rest.
You can try every scenario yourself on the accessibility tests page, switching between implementations, and compare the exact markup on the variants index.
The recommendation at a glance
| Change | Recommendation | Why | Evidence |
|---|---|---|---|
Reveal hidden content when it (or a child) receives focus (:focus-within) |
Keep — already in core | Prevents a keyboard user from landing on a link that stays invisible. The GOV.UK and A11Y Project patterns fail this case. | Measured on WebKit and Blink: focus-within-reveal.spec.js asserts the wrapper reveals for :focus-within and stays hidden for :focus-only. See at-findings.md §3. |
| Add invisible spaces around hidden text so words are not run together | Adopt | Fixes real "blockin" concatenation. Neutral where it is not needed. Not solved by white-space: nowrap alone (different mechanism). |
NVDA 2026.1.1 + Chrome 151 concatenated without it, clean with it (reported in the Drupal a11y review). VoiceOver capture: evidence/boundary-speech.json. See at-findings.md §1. |
Simplify the "skip to main content" target (drop the empty tabindex="-1" anchor) |
Adopt | Modern browsers resume focus at the target without the old workaround. One screen-reader reading-position check still open (below). | Measured on WebKit and Blink: skip-focus.spec.js asserts the next Tab after activating the skip link lands inside <main> for every target variant. See at-findings.md §2. |
Use modern clipping (clip-path) instead of the deprecated clip |
Optional cleanup | Modern idiom; fine to adopt as tidy-up, not as a fix. | No user-facing difference found in testing; a stylistic modernization, so the evidence is the absence of a behavioral delta rather than a positive result. |
Apply the reveal to all hidden content, not only elements marked focusable |
Follow-up | More correct in principle (a focused-but-hidden element is always a bug), but changing the base behavior on hundreds of thousands of sites is a migration risk. | Design argument, not a measured result. Raised in the Drupal a11y review; recorded as an open decision on the implementation breakdown. |
Prevent copying hidden text (user-select: none) |
Not in this change | A separate UX choice, not needed to hide content. If ever added it must include the -webkit- prefix or it does nothing in Safari. |
Not tested here (out of scope). The prefix requirement is a documented Safari behavior; GOV.UK relies on its build to add it. |
Scope: this lives in more than one place
An important practical point for whoever implements this. The rule for hiding content is duplicated in several files in Drupal core, not defined once. A site-wide change has to update all of them, or they will drift:
| File | Role | State |
|---|---|---|
system/css/components/hidden.module.css | The canonical, site-wide rule | The primary target; already uses focus-within |
misc/dialog/off-canvas/css/utility.css | Off-canvas dialogs keep their own copy | Has the old bug: uses :focus only, so it does not reveal a hidden wrapper when a child is focused |
modules/navigation/css/base/admin-reset-styles.css | The Navigation module keeps another copy | Uses the old clip pattern |
The most valuable structural improvement may be to consolidate these to one rule rather than maintaining three copies that already disagree with each other — the off-canvas copy is missing the focus-within fix that core added.
What is still open before final sign-off
- Screen-reader reading position on the skip link. Keyboard focus resumption is confirmed; whether NVDA moves its reading cursor into the main content after activating the skip link is being checked. See the testing plan.
- Is
margin: 0load-bearing? GOV.UK keeps it citing a VoiceOver announcement-order bug with negative margins. There is now a test for it; result pending. - JAWS, mobile screen readers, and 400% magnification have not been tested yet.
None of these block the two proven changes; they refine confidence and settle the follow-up decisions.
Why not just copy GOV.UK or The A11Y Project?
Both are excellent and widely used. But testing showed that both fail one case Drupal needs: when a focusable link sits inside a hidden wrapper, their :focus-only rule leaves the wrapper — and the link — visually hidden. A keyboard user can focus something they cannot see. Drupal's utility is more general and already handles this with :focus-within, so matching GOV.UK or A11Y Project exactly would be a regression. GOV.UK's clipboard protection is a nice extra, but it is a separate choice.
The evidence is focus-within-reveal.spec.js, which measures the wrapper's clipped state after a descendant is focused: it reveals for :focus-within (proposed, current Drupal) and stays hidden for the GOV.UK and A11Y Project :focus-only rules, on both WebKit and Blink. The full comparison with all measured results is on the implementation breakdown.