Layout

The page frame: Header, Sider, Content and Footer, with a sider that collapses and reports when it does.

Accessibility

Layout's accessibility is mostly about the document, not a control. It sets most of your landmarks, and its one interactive part is not a button.

What each part renders

Four of the five parts render a sectioning element, so Layout hands you most of the page's landmark structure whether you asked for it or not. The fifth, the Layout root itself, is a plain <div> and contributes nothing.
  • Layout.Content renders <main>, which is the main landmark. A page gets one. A second Content anywhere in the tree is a second main, and assistive technology offers both.
  • Layout.Header renders <header> and Layout.Footer renders <footer>. Each is a banner or contentinfo landmark only while it is not inside a sectioning element. Layout's own wrappers are divs, so in the standard shell both are landmarks. Put a Header inside a Sider and it stops being one.
  • Layout.Sider renders <aside>, which is complementary. Complementary is not navigation. A rail holding the product's primary navigation needs a <nav> inside it, and Menu does not render one either.
  • Two of the same landmark need names. A page with a navigation rail and a table-of-contents rail has two complementary regions announced identically. Give each an aria-label, or point aria-labelledby at the heading it already has.
The kit changes no element, role or attribute here. It only swaps the Sider's trigger glyph, which is decorative.
In the standard shell a keyboard reader lands on the header, tabs through every item in it, then through every link in the rail, and only then reaches the page. With a 30-item Menu that is 30 presses before the content, on every route change.
  • Put a skip link as the first focusable thing in the document, before the Layout, pointing at the Content: give Layout.Content an id and a tabIndex={-1} so the browser will move focus there and not merely scroll.
  • The link has to be reachable, not invisible. Hide it with a position, not with display: none or visibility: hidden, and reveal it on :focus-visible. sr-only plus focus:not-sr-only does both.
  • One skip link is usually enough, because a landmark-aware reader can jump by region anyway. The skip link is for the sighted keyboard user, who cannot.
  • A shell that persists across routes has to move focus on navigation, or the reader stays parked in the rail while the main region changes under them. The router handles that, but the problem shows up in the Layout.

The collapse trigger is not a button

This is the one thing here you have to fix yourself. The collapse bar is a <div> with an onClick, and the zero-width tab as a <span> with an onClick. Neither carries a role, a tabIndex, an accessible name or an aria-expanded. A keyboard reader cannot reach either one, and a screen reader is not told they exist.
  • Pass your own node to trigger and make it a real button. It renders inside the clickable div, so the div's handler still fires on a click, and the button adds the tab stop and keyboard activation the div lacks.
  • Name it by state, or name it once and state it separately. “Collapse sidebar” against “Expand sidebar”, or a constant “Sidebar” with aria-expanded tracking collapsed. Not both changing at once: that announces twice.
  • The chevron and the menu glyph are decoration. They are not the accessible name, and the default glyphs carry none.
  • A trigger somewhere else is often the better answer. Control collapsed from a real Button in the header, and pass trigger={null} so there is one control rather than two. On a zero-width Sider, remember trigger={null} removes the floating tab too, so that header button becomes the only way back.

Focus and reading order at zero width

Collapsing to collapsedWidth={0} looks like the rail is gone. It is not. The Sider's width goes to zero and its children are clipped, which only changes what is painted. Nothing leaves the accessibility tree or the tab order.
  • Every link in that collapsed rail is still focusable. A sighted keyboard user tabs into a region they cannot see, and the focus ring is clipped by the overflow, so the page looks like it has lost focus entirely.
  • Do not paper over it with tabIndex={-1} on each link. That leaves them in the accessibility tree, so a screen reader still reads a menu the reader has no way to operate.
  • Stop rendering the children when the Sider is collapsed to zero. You already have the state, because a zero-width Sider that reacts to a breakpoint is worth controlling anyway. Returning null inside the Sider is one condition and it fixes both the focus order and the reading order at once.
  • The tab sits outside the Sider's edge and is not clipped, so it stays whatever you do to the children, and emptying them is safe.
  • Hiding the rail also changes the reading order: the Content is now the first region after the header. Make sure the skip link still points somewhere sensible, and that focus is not left on an element that just left the page.

Contrast between the frame and the page

Layout paints four surfaces from four separate component tokens: bodyBg on the Layout root, headerBg with headerColor, footerBg, and siderBg with triggerBg and triggerColor for the bar. A light-theme Sider swaps to lightSiderBg, lightTriggerBg and lightTriggerColor.
  • The Sider defaults to theme="dark" whatever the page is doing. On a light page the rail is an inverted region, so check everything in it against the dark surface, not against the page.
  • The body background and the surface your cards sit on are two different tokens, and they should differ. If they match, the Content stops reading as a surface for anyone who cannot see shadows.
  • A boundary between two large regions is not held to 4.5:1, but it needs 3:1 where it carries meaning, like the edge of a rail against the body.
  • triggerColor against triggerBg is its own pair, and easy to forget: the trigger bar has a different background from the rail above it.
  • Every one of these is a theme token, so changing a seed changes all of them. Check them after changing the seed colors. The Tokens tab lists which tokens this component reads.

Motion on the collapse

The Sider animates its own width with transition: all motionDurationMid, except the background, which never fades. The trigger bar uses the same duration.
  • A width transition reflows everything beside it, on every frame. It is the largest motion in the shell and the most expensive, since the whole content column is laid out again during the collapse.
  • Turn it off for readers who asked, with ThemeScope or the provider carrying token: { motion: false } behind a prefers-reduced-motion query. The Sider then snaps between the two widths. The state change still reads, without the animation.
  • Never nest a bare ConfigProvider to do this. A nested provider re-themes the Sider while the plain divs beside it keep the outer theme. Use ThemeScope, described on the Theming page.
  • Anything you animate on top (a label fading out as the rail narrows, a logo shrinking) belongs behind motion-safe:. The motion token does not reach your own utilities.