Stacksheetv1.1.4

Accessibility

Focus trapping, keyboard navigation, and ARIA attributes.

Focus management

Focus trap

The top panel is wrapped in a focus trap via focus-trap-react. This means:

  • Tab cycles through focusable elements within the panel
  • Focus cannot escape to the page behind the sheet
  • allowOutsideClick: true allows backdrop clicks to close the sheet

Focus restoration

When the sheet stack opens, Stacksheet captures document.activeElement (the trigger element). When the stack fully closes, focus returns to that element automatically.

For stacked sheets, each level handles its own focus restoration via focus-trap-react's returnFocusOnDeactivate — popping a sheet returns focus to the element that was focused in the sheet below.

Fallback focus

If no focusable elements exist inside a panel, the panel container itself (which has tabIndex={-1}) receives focus as a fallback. This ensures the focus trap always has a target.

Keyboard navigation

KeyAction
EscapePops the top sheet. If it's the last one, closes the stack.
TabCycles through focusable elements in the top panel
Shift+TabCycles backward through focusable elements

Escape behavior can be disabled with closeOnEscape: false:

createStacksheet({ closeOnEscape: false });

Android back gesture

On Chromium 120+ browsers, Stacksheet registers a CloseWatcher to handle the Android system back gesture. This lets users swipe back to pop or close sheets, matching native Android behavior.

This is progressive enhancement — on browsers without CloseWatcher support (Safari, Firefox), Escape key handling still works normally.

ARIA attributes

The top panel gets role="dialog" and aria-modal="true". Sheets below the top have these attributes removed.

aria-label

Configure the dialog's accessible name:

// Global default
createStacksheet({ ariaLabel: "Settings panel" });

Per-sheet override via the __ariaLabel prop:

// Component-direct pattern
open(UserProfile, { userId: "abc", __ariaLabel: "User profile for John" });

// Type registry pattern
open("user-profile", "user-1", {
  userId: "abc",
  __ariaLabel: "User profile for John",
});

The per-sheet label takes priority over the global config.

Composable mode

In composable mode (renderHeader={false}), the panel uses aria-labelledby and aria-describedby instead of aria-label. These are automatically linked to Sheet.Title and Sheet.Description via matching IDs.

If you omit Sheet.Title, the panel won't have an accessible name. Always include one, even if visually hidden.

Non-modal mode

In non-modal mode (modal: false):

  • aria-modal is not set on the panel
  • role="dialog" is still set on the top panel
  • Focus trap is inactive — the user can Tab to elements behind the sheet
  • Scroll lock is inactive

This is appropriate for sidebars and auxiliary panels where blocking page interaction would be undesirable.

Scroll lock

When the sheet stack is open, body scroll is locked using react-remove-scroll. This library handles scrollbar width compensation automatically — no layout shift when the scrollbar disappears.

Disable with lockScroll: false:

createStacksheet({ lockScroll: false });

On this page