Problem/Motivation
When a toolbar tray (for example Shortcuts) is open, screen reader users experience focus jumping when attempting to interact with page content.
Instead of landing on the intended element (such as a text field, dropdown, or link), the screen reader focus jumps to toolbar tray items such as Status report or other shortcut links.
This makes it extremely difficult to interact with page content while the toolbar tray is open.
Example:
Open the Shortcuts tray.
Navigate to a form field on the page (for example a dropdown or text field).
Attempt to activate the field.
Expected behavior:
Focus lands on the intended page element.
Actual behavior:
Focus jumps to a link inside the toolbar tray.
This issue affects multiple screen readers.
Tested with:
VoiceOver (macOS Safari)
VoiceOver (iOS Safari)
NVDA (Windows Firefox)
JAWS (Windows Chrome)
Steps to reproduce
Log into Drupal as a user with the admin toolbar enabled.
Open the Shortcuts tray.
Navigate to page content containing interactive elements (forms, dropdowns, buttons).
Attempt to interact with the page element.
Result:
Screen reader focus jumps to toolbar tray items instead of the selected page element.
Investigation
Drupal toolbar trays use fixed positioning.
Example CSS behavior:
position: fixed;
z-index: high
height: 100%
Because of this, the tray remains present in the accessibility tree even when users interact with page content.
Screen readers such as VoiceOver perform spatial hit testing using the accessibility tree.
As a result, screen reader navigation can land on toolbar elements instead of the page content behind them.
Additionally, Drupal’s toolbar implementation currently performs no accessibility focus management for tray state changes.
Specifically:
Trays are visually hidden via CSS
Elements remain focusable
No focus containment exists
No accessibility tree management exists
Initial attempted fixes
Several potential fixes were investigated during debugging.
Attempt 1 — Switching to aria-expanded
Initial testing attempted switching from:
aria-pressed
to:
aria-expanded
However, VoiceOver behaves inconsistently when aria-expanded is used on elements with:

VoiceOver announced:
collapsed
even when the tray was open.
Because of this, the change was reverted.
The original attribute remained:
aria-pressed
Attempt 2 — Using aria-hidden with focus events
A focus handler was implemented to toggle aria-hidden when focus entered or left the toolbar.
Example approach:
$(document).on('focusin.toolbar', function (event) {
However this approach failed because:
VoiceOver virtual cursor navigation does not fire focus events.
Screen reader swipe navigation therefore bypassed the focus logic entirely.
Attempt 3 — Using inert with focus handlers
A similar attempt used the inert attribute combined with focus handlers.
Example concept:
tray.setAttribute('inert', '')
However this suffered from the same limitation.
Because VoiceOver navigation does not fire focus events, the tray remained accessible in the accessibility tree.
Focus jumping still occurred.
Root Cause
The issue occurs because:
Toolbar trays use fixed positioning
They remain visible in the accessibility tree
VoiceOver navigation does not trigger focus events
No accessibility state changes occur when trays open
As a result, screen readers can navigate into tray content while interacting with page elements.

Final Patch Strategy

The final solution uses the inert attribute exclusively. No aria-hidden is needed because the inert attribute already implies ariaHidden per the HTML spec.

1. Inert attribute for closed trays

Closed trays start with inert, ensuring they cannot receive focus, cannot be interacted with, and are removed from the accessibility tree.

this.$el.find(`.toolbar-tray`).each(function () {
this.inert = true;
});

2. Keep open tray inert by default

When a tray opens visually, it stays inert. This prevents screen reader navigation from entering the tray unintentionally while still showing the tray visually.

3. Allow explicit access via Tab

Screen reader users can intentionally enter the tray by pressing Tab on the active trigger, which removes inert.

this.$el.on(`keydown.toolbar`, `.trigger`, function (event) {
if (event.key !== `Tab` || event.shiftKey) {
return;
}
const activeTray = self.model.get(`activeTray`);
if (!activeTray || !$(this).hasClass(`is-active`)) {
return;
}
activeTray.inert = false;
const firstFocusable = activeTray.querySelector(
`a[href], button:not([disabled]), input:not([disabled])`
);
if (firstFocusable) {
event.preventDefault();
firstFocusable.focus();
}
});

4. Restore inert when leaving the tray

When focus leaves the tray, inert is restored.

this.$el.on(`focusout.toolbar`, `.toolbar-tray`, function () {
const tray = this;
setTimeout(function () {
if (!tray.contains(document.activeElement)) {
tray.inert = true;
}
}, 0);
});

5. Restore inert when tray closes

When a tray is closed, inert is restored.

if (previousTray) {
$(previousTray).removeClass(`is-active`);
previousTray.inert = true;
}

Code style notes addressed from review feedback

Uses .inert = true and .inert = false property syntax instead of setAttribute and removeAttribute, per Curtis Wilcox review.

No aria-hidden needed. The inert attribute already implies ariaHidden per the HTML spec, per Curtis Wilcox review.

Results

After applying the patch, screen reader focus no longer jumps into toolbar trays. Page forms and interactive elements can be used while a tray is open. Toggle button announcements remain correct. Escape closes the tray and restores focus. Keyboard users can still access tray content via Tab.

Accessibility impact

This fix significantly improves usability for screen reader users interacting with the Drupal toolbar. Without this fix, users must manually close the toolbar tray before interacting with page content.

Merge request
https://git.drupalcode.org/project/drupal/-/merge_requests/15197

Related issue
Related but separate from:
Issue #3090120
"Use ARIA disclosure pattern for Toolbar buttons with trays"
That issue focuses on ARIA attribute semantics.
This issue addresses a deeper problem involving accessibility tree management and focus behavior.

CommentFileSizeAuthor
toolbar-tray-focus-jumping-fix 3.patch5.14 KBykhalid

Issue fork drupal-3577228

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

ykhalid created an issue. See original summary.

ykhalid’s picture

Version: main » 11.3.x-dev
Priority: Major » Normal
Issue summary: View changes
quietone’s picture

Version: 11.3.x-dev » main

The issue summary reads like it was generated using an AI tool. The policy on the use of AI when contributing to Drupal includes that the use of AI must be disclosed. I suggest reading that to be aware of the expectations of the Drupal community and the consequences for violations of the policy.

And issues are fixed on main first, so changing version.

quietone’s picture

Status: Active » Postponed

However, the Toolbar Module was approved for removal in #3476882: [Policy] Move Toolbar module to contrib.

This is Postponed. The status is set according to two policies. The Remove a core extension and move it to a contributed project and the Extensions approved for removal policies.

The deprecation work is in #3484850: [meta] Tasks to deprecate Toolbar module and the removal work in #3488828: [meta] Tasks to remove Toolbar module.

Toolbar will be moved to a contributed project before Drupal 12.0.0 is released.