Hiding content in Drupal 12: the decision

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

What to change, and how sure we are
ChangeRecommendationWhyEvidence
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:

Where the hide rule is defined in Drupal core
FileRoleState
system/css/components/hidden.module.cssThe canonical, site-wide ruleThe primary target; already uses focus-within
misc/dialog/off-canvas/css/utility.cssOff-canvas dialogs keep their own copyHas 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.cssThe Navigation module keeps another copyUses 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

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.