Spec version v0.10.0

Page Shell

Purpose

The page shell is the top-level application frame for Perforce products built on Force UI. It establishes the persistent chrome — a fixed topbar and a fixed left sidebar — and carves out the scrollable main content area that individual pages populate. Use the page shell when the product needs a persistent horizontal chrome strip: multiple product areas as top-level navigation, an app switcher for cross-product navigation, global search, or a persistent brand identity strip with the product logo and global utility icons (avatar, settings, notifications). For products with a single navigation tier that do not need a topbar, see the sidebar-only-shell pattern. See the Layout Selection section in design.md for the full decision framework. The page shell is never used for marketing pages, login screens, or full-screen wizards.

Structure

The shell is composed of three fixed regions and one scrollable region. The topbar spans the full viewport width at the top. The sidebar runs from directly below the topbar to the bottom of the viewport along the left edge. The main content area fills all remaining space to the right of the sidebar, beginning below the topbar.

+-----------------------------------------------------------------------+
|  TopBar  (full width, 64px tall, fixed, z-index 300)                  |
+------------------+----------------------------------------------------+
|                  |                                                     |
|  Sidebar         |   ╭──────────────────────────────────────────╮     |
|  256px wide      |   │  Content Panel (inset rounded panel)     │     |
|  fixed position  |   │  background: --force-color-bg-surface    │     |
|  below topbar    |   │  border-radius: --force-radius-xl (12px) │     |
|  to bottom       |   │  padding: --force-layout-content-padding │     |
|                  |   │  overflow-y: scroll                      │     |
|                  |   │  fills available width                   │     |
|                  |   ╰──────────────────────────────────────────╯     |
|                  |   ↑ grey chrome (bg-emphasis) shows through        |
+------------------+----------------------------------------------------+

The TopBar occupies the topmost horizontal strip. Its height is fixed at --force-layout-header-height (64px). It sits at z-index: --force-zIndex-fixed (300), which ensures it remains above sticky table headers (--force-zIndex-sticky, 200) and all content, but below modal backdrops (--force-zIndex-modal-backdrop, 400). The topbar does NOT have a bottom border — the rounded content panel sitting below it on the grey chrome provides all the visual separation needed.

The sidebar is fixed below the topbar, meaning its top value equals the topbar height (64px). Its default expanded width is --force-layout-sidebar-width (256px). When collapsed to icon-only mode, the width contracts to --force-layout-sidebar-collapsed (64px). The sidebar uses the same background as the topbar: --force-color-bg-emphasis. Its right edge does NOT carry a border — the rounded content panel creates sufficient visual separation.

The content panel is an inset rounded surface — this is a key Perforce visual signature. The grey chrome background (--force-color-bg-emphasis) is continuous across the entire viewport behind both the sidebar and the content region. The white content panel (--force-color-bg-surface) sits on top of this grey surface with border-radius: var(--force-radius-xl) (12px), creating a visible rounded frame effect. A small gap (matching --force-spacing-2 or 8px) exists between the panel and the sidebar (left), viewport right edge, and viewport bottom so the grey chrome peeks through. The panel sits flush against the topbar at the top — no gap above the panel — so the topbar and panel meet cleanly without a visible grey strip.

The content panel uses position: fixed to fill the remaining viewport space. Its edges are: top: var(--force-layout-header-height) (flush with topbar bottom edge), left: calc(var(--force-layout-sidebar-width) + var(--force-spacing-2)) (sidebar width plus inset gap), right: var(--force-spacing-2), bottom: var(--force-spacing-2). It scrolls internally via overflow-y: auto — this is the only scrollable region. The topbar, sidebar, and body do not scroll with page content. The <body> element uses overflow: hidden to enforce this.

Its background is --force-color-bg-surface (white). The inner content is padded on all sides by --force-layout-content-padding (24px, which matches --force-spacing-6). The content panel fills the available width between the sidebar and the viewport edge — do NOT cap the panel itself with a max-width. Inner columns (forms, long-form prose, settings pages) may opt in to --force-layout-content-max-width (1400px) as a reading-measure constraint, but the panel itself always fills the shell.

The sidebar itself scrolls internally if its navigation items exceed the available height. This internal scroll is separate from the main content scroll.

Responsive Behavior

At and above the large breakpoint (--force-breakpoint-lg, 992px), the full shell is displayed: topbar, expanded sidebar, and content area side by side.

Between the medium breakpoint (--force-breakpoint-md, 768px) and the large breakpoint, the sidebar enters its shrunk state automatically, showing only icons at --force-layout-sidebar-collapsed (64px) wide. The main content area expands to fill the freed space. Tooltips on sidebar items communicate the item label on hover.

Below the medium breakpoint (--force-breakpoint-md, 768px), the sidebar is removed from the document flow entirely and becomes an off-canvas drawer. A hamburger or panel-toggle button appears in the topbar to open it. When opened, the sidebar slides in from the left as a Sheet overlay at 288px (18rem) wide, placed above the content at z-index: --force-zIndex-modal (500). A backdrop overlay does not cover the topbar. The main content area spans the full viewport width in this state. Closing the drawer returns the sidebar to its off-canvas position; it does not persist as an icon rail on small screens.

The topbar always remains full-width and fixed regardless of breakpoint.

Token Usage

Element Token
Topbar height --force-layout-header-height (64px)
Sidebar expanded width --force-layout-sidebar-width (256px)
Sidebar collapsed width --force-layout-sidebar-collapsed (64px)
Inner-column reading-measure cap (opt-in, not applied to the panel) --force-layout-content-max-width (1400px)
Content area padding --force-layout-content-padding / --force-spacing-6 (24px)
Topbar background --force-color-bg-emphasis
Sidebar background --force-color-bg-emphasis
Topbar border (bottom edge) None — the inset content panel below creates visual separation
Sidebar border (right edge) None — the inset content panel creates visual separation
Content panel border 1px solid --force-color-border-default — reinforces the panel edge, especially in dark mode where surface/emphasis contrast is intentionally low
Content panel background --force-color-bg-surface
Content panel border radius --force-radius-xl (12px)
Content panel inset gap --force-spacing-2 (8px) — left, right, and bottom only; flush against topbar at top
Topbar z-index --force-zIndex-fixed (300)
Sidebar z-index (desktop) below fixed (no z-index override needed when in-flow)
Mobile sidebar z-index --force-zIndex-modal (500)
Sidebar open/close transition duration --force-duration-normal (250ms)
Sidebar transition easing --force-easing-standard

Accessibility

The topbar is wrapped in a <header> element with role="banner". The sidebar is wrapped in a <nav> element with role="navigation" and an aria-label of "Application navigation" to distinguish it from any secondary navigation regions on the page. The main content area uses a <main> element with role="main".

A skip navigation link must be the first focusable element in the DOM. It is visually hidden until it receives keyboard focus, at which point it becomes visible. Its target is the <main> element, allowing keyboard users to bypass the topbar and sidebar on each page load. The link text should read "Skip to main content."

Tab order proceeds: skip link, then topbar interactive elements left-to-right, then sidebar navigation items top-to-bottom, then main content. The sidebar and topbar are not in the tab order relative to each other — they are separate landmark regions. A keyboard user arriving at the page enters the topbar first, moves through it, and then Tab moves into the sidebar. To reach the main content quickly, users invoke the skip link.

When the sidebar is in its off-canvas mobile state and closed, its contents must be aria-hidden="true" and all interactive elements within it must carry tabindex="-1" to prevent focus from reaching hidden content. When opened, focus must be moved to the first focusable element inside the sidebar, typically the close button or the first navigation item. Pressing Escape while the mobile sidebar is open must close it and return focus to the trigger button.

The sidebar toggle button carries aria-expanded reflecting the current state and aria-controls pointing to the sidebar element's id.

Guidance

Once a product adopts the page shell, use it for every authenticated page within that product without exception. Do not create one-off full-page layouts that omit the topbar or sidebar for individual pages; those become inconsistent islands. Not every product needs the page shell — see the sidebar-only-shell pattern for products that do not require a topbar.

The main content area padding (--force-layout-content-padding, 24px) is applied by the shell itself. Individual page components that land inside the main content area must not add their own outer wrapper padding — they would double the spacing. Exception: data tables that intentionally bleed to the edges of the content area should use negative margins to cancel the shell padding at their sides only.

Deciding when to apply a reading-measure cap to an inner column. The content panel always fills the available width; the decision is whether a specific page's inner content should be further constrained:

  • Fill the panel (no cap) for data-dense layouts: dashboards, data tables, list views, detail views, wizards, multi-column forms. These are composed of components designed to use horizontal space — capping them wastes chrome on wide monitors.
  • Apply a reading-measure cap (--force-layout-content-max-width or tighter) for article-like content: long-form prose, documentation/markdown pages, release notes, terms/privacy pages, settings forms. Reading comfort degrades past ~75-character line lengths, so narrow these columns within the panel. When the capped column is the sole content of the page, center it horizontally within the panel (margin-inline: auto) so the empty space is balanced on both sides rather than collecting on the right.

When in doubt, ask: is the content primarily paragraph text, or is it primarily components arranged on a grid? The former opts in to a cap; the latter does not.

Do NOT use --force-color-bg-emphasis anywhere inside the main content area. That background is strictly for the topbar and sidebar chrome. Content areas, cards, and panels within the main region all use --force-color-bg-surface (white) or --force-color-bg-muted (secondary grey) as appropriate.

Do NOT place primary call-to-action buttons in the topbar unless they are global actions that apply universally across all pages (such as a "New" button in a global quick-add menu). Page-specific primary actions belong in the PageHeader within the main content area.

Do NOT put primary actions in the sidebar. The sidebar carries navigation items only. Buttons, form controls, and data actions belong in the main content region.

The <body> element MUST use overflow: hidden in the page shell layout. The content panel is a position: fixed scroll container — it is the only element that scrolls. Page-level body scroll must be suppressed to prevent double-scrolling. This is not the same as modal scroll-locking; it is the shell's default state.

Avoid giving the sidebar or topbar a higher z-index than necessary. The defined z-index scale (--force-zIndex-fixed at 300 for the topbar) already accounts for all stacking interactions. Overriding z-index values arbitrarily will cause stacking conflicts with modals and tooltips.

Composition

The page shell contains but does not specify the TopBar component internals. The topbar layout from left to right: the product logo/wordmark and (when present) the app switcher icon on the left; optional product area navigation or global search in the center; global utility icons (notifications, settings, user avatar) on the right. This matches conventional web application layout — the logo in the top-left doubles as the "home" affordance (reachable by a Fitts's-law zero-distance corner target), and user-specific utilities sit top-right where users expect to find them across the wider web (Google, GitHub, Atlassian, and prior Perforce products all follow this convention). Within each zone, order items by usage frequency: in the right-hand utility cluster, place notifications leftmost (checked more often), settings in the middle, and the user avatar rightmost (opens a menu with sign-out and profile). See the sidebar-navigation pattern for how the sidebar region is populated. Individual pages within the main content area follow either the list-page layout (PageHeader + ControlsBar + DataGrid) or the detail-view composition (PageHeader + TabList + tab content panels).

Utility placement — topbar vs. sidebar footer. When the page-shell pattern is in use (topbar is present), the global user utilities — user avatar, account settings link, and notifications — live exclusively in the topbar-right. They MUST NOT be duplicated in the sidebar footer. The sidebar footer in this configuration contains only sidebar-specific controls (e.g., the sidebar compact-mode toggle). For products without a topbar, see the sidebar-only-shell pattern, which routes those same utilities into the sidebar footer as the fallback home.