Steps to replicate the issue (include links if applicable):
- Open any Wikipedia page (mobile interface) that has collapsible sections i.e. https://en.wikipedia.org/wiki/Apple_Inc.
- The "collapsed sections" that come after the introductory paragraphs of the article, can be expanded and collapsed by clicking/tapping on them
What happens?:
- However, for keyboard-only users (i.e. Tab/Enter/Space key), these sections cannot be interacted with
- And, for blind screen reader users, they do not hear the correct role, or the expanded and collapsed state
- This is a failure of the Web Content Accessibility Guidelines 2.2
This is caused by the collapsed sections being implemented using non-semantic HTML (i.e., not using details and summary elements).
What should have happened instead?:
- Collapsed sections should be able to be keyboard-focused, opened and closed using the keyboard only
- Screen readers should announce the correct role i.e. button, or use details/summary elements.
- Screen readers should announce the expanded and collapsed state i.e. using aria-expanded attributes, or using details/summary elements.
I recommend that Wikipedia switches to using HTML details/summary elements for its expanding/collapsing sections, as they require no JavaScript, and are accessible by default, and require no complex ARIA attributes.
Software version (on Special:Version page; skip for WMF-hosted wikis like Wikipedia): Public Wikipedia
Other information (browser name/version, screenshots, etc.):
I tried to fix the problem myself in two different ways — one is a 'quick and dirty' way, and the other way is a more comprehensive transformation in how collapsible secrtions is implemented.
I cloned Mediawiki, MinervaNeue, Mobilefrontend.
Quick and dirty solution
I noticed the mobile.init.js of MinervaNeue did not initialise Toggler.js, but, MobielFrontend's mobile.init.js does initialise Toggler.js. Toggler.js appears to set correct(ish) ARIA attributes on the collapsible headings.
I modified mobile.init.js of MinervaNeue so it ran the mobile.init.js of MobileFrontend. But this didn't work.
I discovered Toggler.js was applying ARIA attributes on a non-existent element, which is why it wasn't working. So I updated the element it applied the attributes to, and the correct attributes were applied.
Changes I made to Toggler.js:
// before...
$headingLabel
.attr( {
tabindex: 0,
role: 'button',
'aria-controls': id,
'aria-expanded': 'false'
} );
// after
$heading
.attr( {
tabindex: 0,
role: 'button',
'aria-controls': id,
'aria-expanded': 'false'
} );//before $headingLabel.attr( 'aria-expanded', !wasExpanded ); //after $heading.attr( 'aria-expanded', !wasExpanded );
So... Toggler.js could perhaps be fixed up, and it'd *mostly* fix this issue, and you'd also have to ensure Toggler.js runs when using MinervaNeue (I assume). I'm not very familiar with this codebase.
Better, more accessible long-term solution
But, frankly, my preferred solution, would be to use details and summary HTML elements instead of these complex JavaScript/ARIA based expanding/collapsing sections — this should be the goal. One advantage of using the details and summary elements instead, is Reader modes like Safari Reader will expose the content properly, giving an additional accessibility benefit. It also needs absolutely no JavaScript - all interactivity is handled by the browser, and it's fully accessible out of the box.
I also attempted to modify MakeSectionsTransform.js in MobileFrontend so it uses details/summary elements instead of complex JavaScript based disclosure widgets. I got it working pretty well but I can't be bothered finishing the job, so I'm hoping someone more familiar with the codebase can pick up this task. From an accessibility perspective, switching to details/summary elements is far better IMO.
Example code to get details/summary elements working for collapsible sections on mobile Wikipedia:
MakeSectionsTransform.js, makeSections function
/**
* Actually splits the body of the document into sections
*
* @param DOMElement $body representing the HTML of the current article. In the HTML the sections
* should not be wrapped.
* @param DOMElement[] $headingWrappers The headings (or wrappers) returned by getTopHeadings():
* `<h1>` to `<h6>` nodes, or `<div class="mw-heading">` nodes wrapping them.
* In the future `<div class="mw-heading">` will be required (T13555).
*/
private function makeSections( DOMElement $body, array $headingWrappers ) {
$ownerDocument = $body->ownerDocument;
if ( $ownerDocument === null ) {
return;
}
// Find the parser output wrapper div
$xpath = new DOMXPath( $ownerDocument );
$containers = $xpath->query(
// Equivalent of CSS attribute `~=` to support multiple classes
'body/div[contains(concat(" ",normalize-space(@class)," ")," mw-parser-output ")][1]'
);
if ( !$containers->length ) {
// No wrapper? This could be an old parser cache entry, or perhaps the
// OutputPage contained something that was not generated by the parser.
// Try using the <body> as the container.
$containers = $xpath->query( 'body' );
if ( !$containers->length ) {
throw new LogicException( "HTML lacked body element even though we put it there ourselves" );
}
}
$container = $containers->item( 0 );
$containerChild = $container->firstChild;
$firstHeading = reset( $headingWrappers );
$firstHeadingName = $this->getHeadingName( $firstHeading );
$sectionNumber = 0;
$sectionBody = $this->createSectionBodyElement( $ownerDocument, $sectionNumber, false );
$hasPrepended = false;
while ( $containerChild ) {
$node = $containerChild;
$containerChild = $containerChild->nextSibling;
// Insert lead content before the first heading
if ( !$hasPrepended && $this->getHeadingName( $node ) === $firstHeadingName ) {
// Insert lead content before the first heading
$container->insertBefore( $sectionBody, $node );
$hasPrepended = true;
}
// If we've found a top level heading, insert the previous section if
// necessary and clear the container div.
if ( $firstHeadingName && $this->getHeadingName( $node ) === $firstHeadingName ) {
// The heading we are transforming is always 1 section ahead of the
// section we are currently processing
/** @phan-suppress-next-line PhanTypeMismatchArgumentSuperType DOMNode vs. DOMElement */
$this->prepareHeading( $ownerDocument, $node, $sectionNumber + 1, $this->scriptsEnabled );
$sectionNumber += 1;
$sectionBody = $this->createSectionBodyElement(
$ownerDocument,
$sectionNumber,
$this->scriptsEnabled
);
// Move the edit section span into the details block after summary
$editSection = DOMCompat::querySelector($node, '.mw-editsection');
$details = $ownerDocument->createElement( 'details' );
$details->setAttribute( 'id', 'mf-section-' . $sectionNumber );
$details->setAttribute( 'class', 'section-details' );
// Find the actual heading tag (h2–h6) inside the wrapper
$innerHeading = DOMCompat::querySelector( $node, implode( ',', $this->topHeadingTags ) );
$summary = $ownerDocument->createElement( 'summary' );
if ($node->hasAttribute("class")) {
$summary->setAttribute('class', $node->getAttribute('class'));
}
if ( $innerHeading ) {
$summary->appendChild( $ownerDocument->createTextNode( trim( $innerHeading->textContent ) ) );
} else {
// Fallback if inner heading can't be found
$summary->appendChild( $ownerDocument->createTextNode( trim( $node->textContent ) ) );
}
$details->appendChild( $summary );
if ( $editSection ) {
$details->appendChild( $editSection );
}
$details->appendChild( $sectionBody );
$container->insertBefore( $details, $node );
$node->parentNode->removeChild( $node );
continue;
}
// If it is not a top level heading, keep appending the nodes to the
// section body container.
$sectionBody->appendChild( $node );
}
}/* updated main.less */
/* find wherever .collapsible-block has display: none set, and change it to display: block (there's two occurrences) */
.collapsible-block:not( .collapsible-block-js ) {
display: block;
}
.section-details {
border-bottom: 1px solid var(--border-color-muted, #dadde3);
margin-bottom: 0.5em;
summary {
cursor: pointer;
list-style: none;
display: flex;
align-items: center;
gap: 0.5em;
&::before {
content: '▶';
display: inline-block;
font-size: 0.75em;
}
}
&[open] summary::before {
transform: rotate(90deg);
}
}Requirement
Collapsed sections on the mobile site must be fully accessible to keyboard-only users and screen readers:
- Section headings must be keyboard focusable with visible outlines
- Toggling must work with Enter or Space
- Screen readers must identify the elements as interactive
- Expanded/collapsed states must be announced via ARIA or semantic HTML (e.g., <details>/<summary>)
- Screen reader rotor must navigate section headings properly
BDD
Feature: Mobile collapsed sections accessibility Scenario: Keyboard-only user interacts with collapsed sections Given I open a mobile article (e.g., https://en.m.wikipedia.org/wiki/Apple_Inc.) When I navigate using the Tab key Then each collapsible section heading should receive focus and show a visible outline And the heading should be announced as interactive (e.g., role=“button” and aria-expanded state) When I press Enter or Space Then the section should expand or collapse And the section content should appear or disappear accordingly Scenario: Screen reader rotor access to collapsed sections Given I have VoiceOver or TalkBack enabled When I use the rotor to navigate to Headings Then each section heading should be reachable and read with its correct expanded/collapsed state When I double-tap a heading Then the section should toggle And the screen reader should announce the updated state
Test Steps
Test Case 1: Keyboard-only interaction
- Open a mobile Wikipedia article (e.g., https://en.m.wikipedia.org/wiki/Apple_Inc.)
- Use Tab key to navigate through the page
- AC1: Each collapsible section heading receives focus and has a visible outline
- AC2: Section heading is announced as interactive (e.g., “button”, “collapsed”/“expanded”)
- Press Enter or Space on a heading
- AC3: Section content expands or collapses
- AC4: Toggling works for multiple headings
Test Case 2: Screen reader rotor navigation
- On a mobile device, enable a screen reader (VoiceOver or TalkBack)
- Open the same article in mobile view
- Use rotor/quick navigation to switch to Headings
- Swipe to navigate between section headings
- AC5: Each heading is announced with its level and expanded/collapsed state
- AC6: Each heading appears in the rotor navigation order
- Double-tap a heading
- AC7: Section toggles open/closed
- AC8: Screen reader announces state change (e.g., “expanded”)
QA Results - Beta
| AC | Status | Details |
|---|---|---|
| 1 | ✅ | T395024#10873332 |
| 2 | ✅ | T395024#10873332 |
| 3 | ✅ | T395024#10873332 |
| 4 | ✅ | T395024#10873332 |
| 5 | ✅ | T395024#10873332 |
| 6 | ✅ | T395024#10873332 |
| 7 | ✅ | T395024#10873332 |
| 8 | ✅ | T395024#10873332 |
