Skip to content

Patterns · Layout

AppShell

The signed-in Rook layout: rail, optional identity bar and main, with a skip link and contained scrolling.

@rookcom/monarch-react/shellPattern

Rail, identity bar and a scrolling main region

Usage examples

The example at the top of the page is the only one for this component.

Purpose

Lays out every signed-in Rook app the same way and gets the page-level accessibility right once: the skip link, the landmark order and a main region that scrolls on its own. Needed by: app, calendar, friends, stride, whose layouts each hand-offset content by the rail width and had no skip link.

Use when

  • The root layout of a signed-in app (app/(workspace)/layout.tsx).

Avoid when

  • Public and marketing pages.
  • A two-pane area inside a page: use SplitLayout.

Design principles

  • No magic offsets: the rail is a grid column, so content never needs padding-left: var(--rail-width).
  • The first Tab stop skips the navigation.
  • One scrolling region by default, so the rail and header never scroll away and the page never double-scrolls.
  • Server-safe: no state, no effects, no event handlers.

API reference

import { AppShell } from '@rookcom/monarch-react/shell';
ExportKindRenderingRoot class
AppShellcomponentserver-safe.mn-app-shell

Forwards its ref to the root <div>. children render inside <main>.

Generated from the TypeScript declarations of packages/react/src/patterns/layout/app-shell.

Properties

mainProps passes attributes (an aria-label, a class) to <main>.

AppShell

The layout of a signed-in Rook app: the rail on the left (a top bar and drawer on phones), an optional identity bar on top, and the main region. It adds the skip link (the first Tab stop; `<main>` takes focus because it has `tabindex="-1"`), keeps the landmarks in order (navigation, banner, main) and contains scrolling to the main region. Server-safe: no state.

PropTypeDescription
childrenReactNodeThe page. Rendered inside `<main>`.
classNamestring
headerReactNodeThe top bar, usually `<IdentityBar />`.
idstring
mainIdstringId of `<main>`, the skip link's target. Default `main`.
mainPropsOmit<HTMLAttributes<HTMLElement>, "id" | "children">Extra props for `<main>` (an `aria-label`, a class).
mainRole"main" | "region"`main` (default), or `region` for a shell embedded in another page that already has its `<main>` (docs, previews, onboarding tours): the content becomes a `<div role="region">`, named by `mainProps['aria-label']`.
railReactNodeThe primary navigation, usually `<RookRail />`.
scroll"main" | "page"`main` (default): the shell fills the viewport and only the main region scrolls, so the rail and header stay put. `page`: the document scrolls and the rail and header are sticky (for long, printable pages).
skipLinkLabelstringText of the skip link. Default "Skip to content".

Variants

PropValuesDescription
mainRole'main''region'region renders the content as a named <div role="region">, for a shell embedded in a page that already has <main>.
scroll'main''page'main (default): the shell fills the viewport and <main> scrolls. page: the document scrolls; the rail and identity bar stay sticky.

States

StateExposed asBehaviour
Scroll modedata-scroll="main|page"On the root.
With rail / headerdata-rail, data-headerSet when the slot is used.

Interaction behaviour

  • The skip link appears when focused and moves focus to <main> (it has tabindex="-1", so it is not a Tab stop itself).
  • With scroll="main", scrolling stops at the main region’s edges (overscroll-behavior: contain).
  • Printing hides the rail, header and skip link and lets the content flow.

Keyboard support

KeysAction
Tab (first)Shows the "Skip to content" link.
EnterOn the skip link: focus moves to the main region.

Accessibility

Role
Landmarks in order: the rail (complementary + navigation), banner (identity bar), main
Accessible name
<main> needs no name; add one with mainProps["aria-label"] if the page has several regions.
  • Adds the skip link every audited app was missing (WCAG 2.4.1 Bypass Blocks).
  • The main region shows a focus outline only when reached from the keyboard.
  • With scroll="main", keyboard users scroll main by moving through its focusable content (or after the skip link focuses it); a page with no focusable content at all should use scroll="page".

Responsive behaviour

  • 768px and up: rail column on the left, identity bar and main to its right.
  • Below 768px: one column; the rail becomes its top bar (with the drawer), then the identity bar, then main.
  • Uses 100dvh, so mobile browser toolbars do not hide content.

Theme support

  • Canvas background and text tokens; the rail and bar bring their own surfaces.

Do and don't

Do

  • Use it once, at the root of the signed-in layout.
  • Start each page with a heading inside main (PageHeader).
  • Use scroll="page" for long reading or printable pages.

Don't

  • Do not add your own rail offset or overflow on body.
  • Do not render a second <main> inside it.

Dependencies

KindDepends on
npm packagesreact
Monarch componentsNone

Testing coverage

  • Landmarks in order: navigation, banner, main; default scroll mode.
  • Skip link is the first Tab stop and targets main, which is not a Tab stop.
  • Custom main id, skip text and main props; page scrolling; no rail or header; embedded as a named region.
  • Server render.
CheckCovered
Axe in jsdomYes
Server renderingYes
Browser suitesVisual baselines: /__gallery/app-shell; Cross-browser axe: /components/app-shell

Tests: packages/react/src/patterns/layout/app-shell