Skip to content

App shell

Composites

All elements · App shell on the Composites page

Last checked 2026-10-22 · Awaiting approval · Accessibility tests

Copies the bun run a11y:approve command for App shell.

App shell

nav + appbarWCAG 2.2 AA · Passed

One grid of five children — brand, utility, nav, app bar and content — sharing a single placement map. It is the only arrangement whose reading order matches DOM order, so focus never jumps back up the page. Row one is the legacy masthead (logo on white, the navy .topMenu hugging the right corner). The nav-toggle button collapses the nav, whose rows have no icons, so the rail leaves the grid and the app bar and content take its column; on mobile it becomes a drawer. AppBar tabs are added dynamically by pages.

The full-screen page is shell.html, styled by css/classic/composites/shell.css. The specimen below is copied from shell.html by bun scripts/design-pages/build-composite-pages.mjs — edit that file, not this one; --check fails when they drift. The same run copies its shell around every other composite's full-screen page.

Axis Values
.qf-shell[data-nav] expanded · collapsed · open (mobile drawer)
.qf-shell__brand › .qf-shell__logo | .qf-shell__logo-placeholder configurable SVG logo, or the dashed “Add logo here” slot when none is set
.qf-shell__utility › .qf-shell__links · .qf-shell__identity secondary destinations and the one signed-in identity; rows are .qf-nav__item, each with an icon in a .qf-tooltip-host. When the labels no longer fit on one line (a narrow window or zoom), data-compact shows the icons and moves each label into a bottom-end tooltip. At 30rem or less the links fold into a .qf-shell__more (vertical ellipsis) menu, keeping the masthead one row. The identity opens a menu: the signed-in time, Profile, the Dark theme switch (menuitemcheckbox, the menu stays open), and Sign out. The utility strip carries no switches of its own.
.qf-nav__item[aria-current=page] · __badge · __section navigation parts: the rail is SideNavigation (C-50), placed by .qf-shell > .qf-nav
.qf-nav__row › .qf-nav__twisty · .qf-nav__item · .qf-nav__action a row for what the link cannot contain: a group disclosure (.qf-nav__subitems) or a row action such as “new document”. Plain destinations stay a bare .qf-nav__item.
.qf-appbar__tab[aria-selected] · __search · .qf-bell app bar parts
.qf-page-header · .qf-breadcrumbs content header: the title, subtitle and actions, with the path above them as a Breadcrumb (C-49)
Shell (toggle the nav with the menu button)
Skip to content
Mobile coming soon
Please use a desktop width.

Guidance

When to use

  • Use it once per page as the frame around every QAction screen: brand, utility links, main nav, app bar and content.
  • Use it when users move between top-level areas such as Inbox, Documents and Workflows from a persistent nav.
  • Use the app bar tabs when a page has a few sibling result sets, such as Active, Completed and Delegated.
  • Use nav row actions when a destination needs its own commands, such as New in Documents.

When to consider something else

  • Use Tabs inside the content area when the choices are views of one object rather than page-level result sets. See Tabs.
  • Use a Tree when users browse a deep hierarchy as content rather than as navigation. See Tree.
  • Use OverlaySystem windows for objects opened from results; the shell only provides the window area they live in. See OverlaySystem.

Usability

Do
  • Keep DOM order brand, utility, nav, app bar, content, so reading and focus order match the layout.
  • Mark the current destination with aria-current="page" on its .qf-nav__item.
  • Put a row action (.qf-nav__action, .qf-nav__add) in a .qf-nav__row beside the link; keep plain destinations a bare .qf-nav__item.
  • Configure one logo in .qf-shell__brand; keep the Add logo here placeholder only until the tenant supplies one.
  • Put theme and inbox view settings in the identity menu; the utility strip carries links only.
  • Lead each entry and saved-search row with its .qf-icon (an entry's is fixed: inbox, files, folder, file-search, checklist for Tasks, cube for a custom object; a saved search's is the one picked in the IconPicker, in its color).
  • Show each utility link's icon beside its label; once compact (data-compact) the icon stands alone, named by its tooltip.
Don’t
  • Do not put an icon plus text in the brand slot; it holds one logo.
  • Do not hide a nav-toggle in classic with CSS; the classic page has no toggle button at all.
  • Do not nest a button inside a nav link to add a row action; use .qf-nav__row.
  • Do not add more than a few app bar tabs; long sets belong in the nav or a saved-search menu.

Accessibility

Keyboard

App shell keyboard
Keys Action
Tab Moves through utility links, nav rows and their buttons, the nav resize handle, the app bar and then the content, in DOM order.
Enter Follows a nav link, or expands and collapses a group from its twisty button.
Shift+F10 or Context Menu On a nav row, opens that row's menu and focuses its first item.
Arrow Left / Arrow Right On the app bar tabs, moves to and selects the previous or next tab; Home and End go to the first and last.
Arrow Left / Arrow Right On the nav resize handle, narrows or widens the nav by 1rem; Home and End jump to the minimum and maximum width.
Arrow Down / Arrow Up In an open utility, identity or row menu, moves between items; Escape closes the menu and returns focus to its button.

Roles and names

  • The main nav is <nav class="qf-nav" aria-label="Main">, the utility strip is <nav class="qf-shell__links" aria-label="Utility">, and the app bar is a <header>.
  • The binding must render .qf-content as the page's <main> landmark; the reference pages get <main> from the site frame.
  • Twisty buttons carry aria-expanded and aria-controls, and their aria-label reads Expand or Collapse plus the group name; subgroups are role="group" with aria-labelledby.
  • The nav toggle carries aria-controls and aria-expanded; the resize handle is role="separator" with aria-orientation="vertical", aria-valuenow, aria-valuemin and aria-valuemax.
  • App bar tabs are role="tablist" and role="tab" with aria-selected; identity settings are menuitemcheckbox and menuitemradio items that keep the menu open.
  • Nav rows with a context menu advertise it with aria-keyshortcuts="Shift+F10".
  • The shell's first child is a .qf-skip-link to the .qf-content (<main>, tabindex="-1"): hidden until it takes focus, then drawn at the top-left with the focus ring (2.4.1).

Focus

  • Provide a skip link to <main> as the first focusable element on the page.
  • When the mobile drawer (data-nav="open") opens, move focus into the nav and make the rest of the shell inert; Escape must close it and return focus to the toggle.
  • Closing a menu with Escape returns focus to the button that opened it.
  • In compact mode (data-compact) utility labels move into tooltips; each link must keep its text as its accessible name.

Other requirements

  • The classic skin replaces the whole shell with a Mobile coming soon gate at 30rem and below, which fails 1.4.10 Reflow at 320 CSS pixels; resolve before claiming conformance.
  • Twisty, row action and add buttons must meet 24 by 24 CSS pixels or have equivalent spacing (2.5.8).
  • The drawer slides in with a transform transition; honor prefers-reduced-motion (2.3.3).
  • At 200% zoom the app bar's tablist shrinks and scrolls inside the bar instead of being clipped (1.4.10); collapsing the nav never hides the masthead's utility labels.

Test your implementation too. This guidance covers the element as specified here. How a screen composes it (labels, order, announcements) still needs its own keyboard and screen reader check.

Accessibility tests

WCAG 2.2 AA · Passed

Passed the gate (run 358b316 on 2026-10-22): no finding in any theme. Awaiting a person’s approval of the checklist below.

App shell test checklist
Area Automated A person checks
General Passed Color is never the only cue, text alternatives say the same thing as the graphic, and the element reads the same in the light and the dark theme.
Zoom Passed At 200% zoom, at 320 CSS pixels wide and with the WCAG text-spacing overrides, nothing is lost, clipped or overlapping.
Keyboard Passed Every row of the Keyboard table works, focus is always visible and never trapped, and the tab order follows the reading order. (6 rows)
Screen reader Passed With NVDA or JAWS on Windows and VoiceOver on macOS, names, roles and states are announced as Roles and names says, and changes are announced once. (7 items)

Approving. When the checklist passes on the checked run, record it with bun run a11y:approve -- shell --by "Your name" and commit a11y/approvals.json with the rebuilt pages. All test status

Package

Consumers link one stylesheet, css/classic/index.css, which includes every element. The files below are where this element is defined.

App shell package
Source css/classic/composites/shell.css
css/classic/components/actions.css
css/classic/base.css
Specimens also use Avatar · Box · Button · Card · Chip · Divider · EmptyState · Icon · Menu · SideNavigation · Table · Text · Toolbar · Tooltip
Sprites sprites/icons.svg
React Not yet available: the React binding has not started.

Latest updates

App shell latest updates
Date Version Breaking Description
2026-10-20 0.1 No Footer (C-51): the app's page footer (footer.qf-footer, the contentinfo landmark): __product (__name, __version), a nav.__links of the accessibility statement, privacy, terms and support (24px targets), and the __legal copyright line; data-variant="compact" puts it on one line (components/footer.css). The App shell gains an optional footer slot, .qf-container > .qf-footer, on the chrome under the content; shells without one are laid out as before
2026-10-20 0.1 No SideNavigation (C-50) catalogs the shell's .qf-nav rail as a standalone component: its rules move from composites/shell.css to components/side-navigation.css (rows, twisties, nested __subitems, aria-current="page" and the fill its parents keep, __section, __badge), with a specimen outside the shell. The shell keeps only placement (.qf-shell > .qf-nav: the nav column, clipping, the drawer layer, the list's room under the masthead) and the fillets that join the current row to the app bar, now off outside the shell. Element pages credit .qf-nav to SideNavigation instead of the App shell
2026-10-20 0.1 No Breadcrumb (C-49): the shell's .qf-breadcrumbs is cataloged and its rules move from composites/shell.css to components/breadcrumb.css; the content header (.qf-page-header) keeps using it. nav[aria-label="Breadcrumb"] › ol.__list › li.__item, every level a __link but the current page (aria-current="page"), CSS-drawn separators, 24px targets. Long paths wrap by default; data-overflow="truncate" keeps one line and cuts the levels above the current page first; a deep path hides its middle (__item[hidden]) behind a __more button that reveals it in place
2026-10-20 0.1 Yes Classic is the only skin: css/modern/ and js/skin.js are removed, so consumers link css/classic/index.css directly (no data-skin-sheet swap) and keep data-skin="classic" on <html> for scripts. The skin switch is gone from the site's app bar, the shell's identity menu and the Office review strip; theme stays. The per-skin composite pages are single pages: composites/shell.html, inbox.html, overlay-system.html and search.html replace the *.classic.html and *.modern.html pairs
2026-10-20 0.1 No App shell: a skip link (.qf-skip-link, the shell's first child, to the .qf-content), hidden until focused, then drawn at the top-left with the focus ring rather than a shadow. Classic sidebar rows carry icons (inbox, files, folder, file-search, checklist, cube; a saved search's own icon and color), so the twisty gutter (--qf-nav-gutter) narrows to the twisty's 24px target (--qf-space-6), the item padding (--qf-nav-item-pad) to --qf-space-1, and the label sits --qf-nav-item-gap (--qf-space-2) from its icon. The workflows entry reads Tasks; a saved search's row menu gains Change icon…. The utility links always show their icons beside their labels, alone when compact. At 200% zoom the app bar's tablist shrinks and scrolls inside the bar instead of being clipped; collapsing the nav still hides only the rail, so the masthead's link labels stay. Classic skin
2026-09-13 0.1 No Host composite pages in the app shell and sync specimens from them

All release notes