Pattern · pages
App shell
The frame of every signed-in page: one column of navigation that folds to a rail and becomes a drawer on a phone, a banner for what concerns the whole application, and a content area that says when it is loading.
The pattern, live
Audit Q2 2026, réaudit
La page vit dans la zone principale. Repliez la colonne avec le bouton sous la marque, ouvrez une section depuis le rail, ou jouez une navigation pour voir la zone dire qu'elle charge.
How it works
- The skip link comes first and targets the content area, which is the main landmark with
tabindex="-1", so the link really moves the focus. - The column is one navigation landmark, named. Each section has an icon, learnt while the word is still next to it, a title tied to its list with
aria-labelledby, and its links. There is no heading in the column: it comes before the page's h1. - The fold button says its state with
aria-expandedand keeps one name; the chevron only draws the gesture. Folded, the column is a rail of icons with hidden names, and each section becomes a disclosure: a button that shows a panel of links, one at a time, closed by Escape, by a click elsewhere, or by a navigation. - Under 1024 pixels the column is a drawer, closed at first and out of the tab order, opened by the one button of the top bar, closed by Escape or its backdrop.
- The foot sits outside the navigation landmark: the address, the theme switch, which is a fieldset, and the sign-out, which is a form that posts, never a link.
- The banner concerns the whole application, so the frame carries it above the content area, as a named aside, with no live role, dismissed and remembered.
- The content area is a BusyRegion. A navigation that takes longer than a threshold sets
aria-busyand writes the status region, which sits outside the main landmark:aria-busyon an ancestor would silence it.
Rules a page that copies it keeps
- The page provides the data and the current path; the shell decides the rest. The fold's memory is the page's, written to a cookie in
oncollapse, so the server renders the right width from the first byte. - The wait is written from a navigation helper with a threshold, never from every navigation: a page already in cache would flash and interrupt.
- The status region is emptied when the page arrives, since SvelteKit's own announcer says the new title; two announcements would cut each other.
- The rail's panels are disclosures, not menus: a menu role promises arrow keys, Home, End and type-ahead, and a promise half kept is worse than none.
- The current link says so with
aria-currentand a line, never by colour alone; a finished item shows a check with a hidden word. - A section with no links is not rendered; a section closed to the user shows one sentence with the one link that opens it, and no link to a zone that refuses.
- Print hides the bar and the column; the content area keeps its width.