Role="text" must not contain focusable elements

Last updated:

Who it helps: BlindMobility
Standard: WCAG 2.2 Level A

Elements using ARIA role=text must not include focusable descendants

Elements with ARIA role=text must not contain any child that can receive focus. This role is sometimes used to keep a phrase read as one piece when markup splits it, but it flattens semantics for everything inside. If a link, button, or other focusable control sits inside, assistive tech may treat it as plain text and fail to announce its role or state.

Why It Matters

  • Screen reader users rely on announced name, role, and value. Wrapping interactive controls in role=text hides those semantics, so a link may be read as ordinary text.
  • Keyboard users can tab to an element that provides no meaningful announcement, causing confusion and navigation errors.
  • Cognitive load increases when interactive items are indistinguishable from plain text, making tasks like following links or submitting forms harder.

Common Causes

  • Wrapping a phrase that includes a link or button in a span/div with role=text.
  • Using role=text to fix line breaks or styling instead of CSS.
  • Adding tabindex to descendants inside a role=text container.
  • Applying role=text via a broad CSS/JS selector that unintentionally targets components.
  • Placing contenteditable or custom widgets inside role=text.

How to Fix

  • Limit role=text to purely textual content. Do not include links, buttons, inputs, selects, textareas, iframes, summary, or any element with tabindex≥0 inside.
  • If a phrase contains an interactive item, remove role=text from the wrapper and keep the interactive element separate so its semantics remain intact.
  • To maintain continuous reading across visual markup (e.g., br, em, strong), wrap only the text nodes that need merging and exclude any interactive control from that wrapper.
  • Remove tabindex attributes from descendants within any role=text element. Make the focusable element a sibling instead.
  • Use CSS for presentation (spacing, line breaks) instead of role=text when possible.
  • If you need a grouped announcement for a control, do not hide it inside role=text. Instead use aria-label, aria-labelledby, or visually hidden text to provide the phrase while preserving the control’s role.
  • Recommendation: Add a lint/check step to flag [role="text"] elements that contain focusable descendants in CI.

Focusable descendants to avoid inside role=text include:

  • a[href], button, input, select, textarea
  • [tabindex]:not([tabindex="-1"]) and elements made focusable via scripting
  • summary, iframe, contenteditable=true

How to Test

Keyboard check:

  • Tab through the page. Every stop should announce a clear role (link, button, etc.).
  • If you land on an item inside a role=text wrapper and it is not announced with its role, that’s a failure.

Screen reader check (NVDA/JAWS/VoiceOver):

  • Navigate by links and buttons. Ensure each control inside or near role=text is announced with correct role and label.
  • Read the surrounding text by line/paragraph to confirm noninteractive text in role=text is read naturally.

Mobile/touch check (VoiceOver/TalkBack):

  • Swipe through elements. Verify links and buttons near role=text are identified as interactive and can be activated.

DOM audit:

  • Use a console snippet to find violations.
JS
const nodes = document.querySelectorAll('[role="text"]');
nodes.forEach(el => {
  const focusable = el.querySelectorAll(
    'a[href], button, input, select, textarea, iframe, summary, [contenteditable="true"], [tabindex]:not([tabindex="-1"])'
  );
  if (focusable.length) {
    console.warn('Focusable descendant inside role=text:', el, focusable);
  }
});

Good Example

HTML
<h1>
  <span role="text">New Products <br>Available Today</span>
</h1>
<p>
  <span role="text">Learn more about our shipping options</span>
</p>
<p>
  See our <a href="/help">help center</a> for details.
</p>
Explanation: role=text is used only around plain text and a line break. The interactive link is separate, so its role is preserved.

Bad Example

HTML
<p>
  <span role="text">Submit <button type="button">Send</button> request</span>
</p>
Impact: The button is inside role=text, so some assistive technologies may announce it as plain text rather than a button.

Quick Checklist

  • Do not place links, buttons, form fields, or any tabbable element inside role=text.
  • Use role=text only for purely textual runs that need uninterrupted reading.
  • Keep interactive elements as siblings of role=text wrappers, not descendants.
  • Remove tabindex from any child inside role=text.
  • Prefer CSS for layout/line breaks instead of role-based workarounds.
  • Verify with keyboard and a screen reader that each interactive item announces its role and label.
  • Add automated checks to flag [role="text"] with focusable descendants.