busy-office-ui
Menu

Pattern: app frame

Who uses it: everyone, on every screen — the persistent chrome the whole suite lives inside. How often: it never leaves the screen. What "done" looks like: any module is one click away, and search, notifications and the user menu are in the same place on every screen of every module. The anatomy is the modern consensus across enterprise suites (Fiori's shell bar, Dynamics' area-plus-sitemap, Lightning, Carbon's UI shell): a suite-level top bar, a module-level sidebar, and a main region that belongs entirely to the page. Not for a single-task dedicated device — a handheld scanner runs RF full-screen with no frame at all; and not an iframe — the frame is the layout every page renders inside, not a container that embeds pages.

The screen

Acme ERP Connection: online Alerts 3

The page — including its own breadcrumb and title. Nothing here is the frame's business.

The waffle button and the user menu are native [popover] menus — open them: Escape closes, click-away closes, focus returns to the trigger, none of it our code. The switcher menu is app launch in miniature; its last entry goes to the full launcher.

Frame-owned vs page-owned — the boundary that keeps a suite coherent

Every design system studied draws the same line (GOV.UK made it a published rule: suite-wide tools and service-level tools must not blur). What is true on every screen lives in the frame; everything else belongs to the page:

ElementOwnerWhy
Brand, module switcher, global search, notifications, user menuFrame (top bar)true everywhere; Atlassian's redesign stripped the top bar to exactly this set
Module navigationFrame (sidebar)contextual to the module; swaps whole on module change — never mixed into the top bar
Breadcrumb, page title, page actions, contentPagethe frame must not need to know what any page is about
Which modules exist, their order, role visibilityServerauthorization data, rendered into the frame — never framework opinion

Markup

<!-- The frame is ONE navbar + ONE sidebar around every screen.
     Top bar = suite-level only (true on every screen); sidebar = the current
     module's nav; everything else — breadcrumbs, titles, content — belongs
     to the page. The switcher and user menu are native [popover] menus:
     Escape, click-away and focus return are platform behavior, not ours. -->
<div class="bo-app-shell">
  <header class="bo-navbar bo-app-shell__header">
    <a class="bo-navbar__brand" href="/">Acme ERP</a>

    <!-- Module switcher: the waffle. Reuses the launcher's tile mechanism
         at menu size — the menu is app-launch in miniature. -->
    <button class="bo-btn bo-btn--ghost" type="button" popovertarget="frame-apps"
            aria-label="Switch module">
      <span class="bo-icon bo-icon--grid" aria-hidden="true"></span>
    </button>
    <div class="bo-dropdown__menu" id="frame-apps" popover>
      <a class="bo-dropdown__item" href="/purchasing"><span class="bo-icon bo-icon--cart" aria-hidden="true"></span> Purchasing</a>
      <a class="bo-dropdown__item" href="/warehouse"><span class="bo-icon bo-icon--box" aria-hidden="true"></span> Warehouse</a>
      <a class="bo-dropdown__item" href="/reports"><span class="bo-icon bo-icon--chart" aria-hidden="true"></span> Reports</a>
      <a class="bo-dropdown__item" href="/home">All applications…</a>
    </div>

    <span class="bo-navbar__spacer"></span>

    <input class="bo-input" type="search" aria-label="Search everything"
           placeholder="Search…" style="max-inline-size: 14rem" />

    <!-- Sync-state slot: badge + glyph SHAPE + text (two channels), the
         live region announces changes. States: ● Online / ↻ Syncing /
         ⏸ 3 queued / ✕ Sync error. YOUR app sets them; the frame is only
         the one place they always appear. -->
    <span class="bo-badge bo-badge--success"><span aria-hidden="true">●</span> Online</span>
    <span aria-live="polite" class="bo-visually-hidden">Connection: online</span>

    <a class="bo-btn bo-btn--ghost" href="/notifications">
      Alerts <span class="bo-badge bo-badge--warning">3</span>
    </a>

    <button class="bo-btn bo-btn--ghost" type="button" popovertarget="frame-user">
      <span class="bo-avatar" aria-hidden="true">JK</span> J. Kim
    </button>
    <div class="bo-dropdown__menu" id="frame-user" popover>
      <a class="bo-dropdown__item" href="/profile">Profile</a>
      <a class="bo-dropdown__item" href="/signout">Sign out</a>
    </div>
  </header>

  <nav class="bo-sidebar-nav bo-app-shell__sidebar" aria-label="Purchasing">
    …the CURRENT module's nav (swaps whole when the module changes)…
  </nav>

  <main class="bo-app-shell__main"> …the page, including its own breadcrumb… </main>
</div>

Anatomy

  1. Shell — .bo-app-shell: header + sidebar + main; the sidebar collapses to an icon rail in the shell's narrow container state, labels kept in the accessibility tree.
  2. Top bar — navbar: brand, then the suite-level controls.
  3. Module switcher — a ghost button opening a dropdown of module links; the full grid lives on app launch, which the menu's last entry opens.
  4. Global search — a search input; wire it to the command bar for type-to-jump.
  5. Sync state — a badge whose glyph SHAPE and text both change per state, with an aria-live announcement — the full contract is the offline concept page.
  6. Notifications — a link with a badge count into the notification center.
  7. User menu — avatar + dropdown.
  8. Module sidebar — sidebar nav with aria-current="page" on the active entry; its aria-label names the module.

Data contract

RequestReturns / on failure
GET (any page)the frame renders server-side around every page — modules and sidebar entries already filtered by the user's role; a module the user cannot open is absent, not disabled
GET /notifications/counta fragment swapped into the badge once available; the bar renders WITHOUT it. On failure the badge simply never appears — a silent degrade, same contract as the launcher's tile counts
4xx on a module linka full navigation to an error page — the frame stays; only main is the casualty

States

StateWhat ships
Current modulethe sidebar's active entry carries aria-current="page" + the visible marker — two channels
Narrow shell (container state)the sidebar collapses to its icon rail; labels stay in the accessibility tree. A bottom-nav tier is deliberately NOT shipped — re-opens on a real consumer need
No permission for a moduleabsent from switcher and sidebar both — a disabled module teaches nothing and leaks structure
Count unavailableno badge, no error — the bar never fails loudly over a number
Printthe whole frame disappears (.bo-navbar and the sidebar are print-hidden); paper gets the page, not the chrome
No JSthe switcher and user menus are native popover — they open, close on Escape/click-away and return focus with zero JS; only menu anchoring (initDropdowns) is an enhancement. Search submits as a form; every module link is a real link

Components used

complexity 2 of 4

↑↓ move Enter open Esc close