Skip to content

Patterns · Navigation

RookRail

The shared Rook navigation rail: 48px icons that expand to labels, remembered across apps, with a drawer on phones.

@rookcom/monarch-react/shellPattern

Collapsed, with Help & support and a theme toggle

Usage examples

Expanded, with workspace and Space switchers

Purpose

One primary navigation for every Rook app, so people find the same things in the same place everywhere: the app, its sections, the Workspace block, Help & support, the theme, notifications and their account. Apps pass data, links and slots; the rail owns markup, behaviour and accessibility. One shared component instead of a copy in every product.

Use when

  • The primary navigation of a signed-in Rook app, inside AppShell.
  • Any Rook surface that should match the other apps (shared expand state, shared items).

Avoid when

  • Marketing and public pages: use the marketing site shell from @rookcom/monarch-react/marketing.
  • Secondary navigation inside a page (tabs, settings sections): use Tabs or a sidebar list in the page.

Design principles

  • Data in, chrome out: the app supplies items and slots, never markup, so every Rook app keeps one look and one accessibility baseline.
  • Icons are never the only label: collapsed items keep their label as visually hidden text and show it in a tooltip.
  • The expand choice is the person’s, not the app’s: it lives in the shared rook_rail cookie, and a pre-paint script avoids a flash of the wrong width.
  • On phones the same navigation moves into a modal drawer with a real focus trap, which the copied rails were missing.
  • Active state is not colour alone: a raised background, a heavier weight and an inside bar.

API reference

import { ROOK_RAIL_COOKIE, ROOK_RAIL_EVENT, RookRail, rookRailScript } from '@rookcom/monarch-react/shell';
ExportKindRenderingRoot class
ROOK_RAIL_COOKIEutilityserver-safe–
ROOK_RAIL_EVENTutilityserver-safe–
RookRailcomponentclient.mn-rook-rail
rookRailScriptutilityserver-safe–

rookRailScript({ cookieName? }) returns the <head> script that marks <html data-rook-rail="expanded"> before first paint.

ROOK_RAIL_COOKIE (rook_rail, values expanded | collapsed) and ROOK_RAIL_EVENT (rook-rail on window) are the cross-app contract.

Slots (workspaceSwitcher, spaceSwitcher, themeToggle) take a node, or a function of { collapsed, placement } so a switcher can render compactly in the collapsed rail and in full in the drawer.

Generated from the TypeScript declarations of packages/react/src/patterns/navigation/rook-rail.

Properties

RookRailItem: key, label, icon, href or onSelect, external, opensIn, badge, active/exact, shortcut.

Expand state: uncontrolled and persisted (default), persist={false} (in memory), or controlled with expanded + onExpandedChange (nothing is written).

cookies replaces document.cookie (tests, embedded frames); cookieDomain defaults to .rookhq.com on Rook hosts.

RookRail

The Rook primary navigation rail, shared by every Rook app: a 48px icon rail that expands to labels (remembered across apps in the `rook_rail` cookie), with the app's brand, workspace and Space switchers, the app's sections, the Workspace block, Help & support, a theme toggle slot, notifications and the account menu. Below 768px it becomes a top bar whose menu button opens the same navigation in a drawer (a modal Sheet: focus moves in, is trapped and returns to the button). Apps pass data, links and slots; the markup, behaviour and accessibility are the rail's.

PropTypeDescription
brand requiredRookRailBrandThe current app: its icon, wordmark and home link.
sections requiredRookRailSection[]The app's own navigation, in order.
accountShellAccountThe signed-in person: avatar, account menu and Sign out.
classNamestring
cookieDomainstringDefault: `.rookhq.com` on Rook hosts, otherwise the current host.
cookieNamestringDefault `rook_rail`.
cookiesShellCookieStoreWhere the state is stored. Default `document.cookie`.
currentPathstringThe current path, matched against item hrefs for the active item.
defaultExpandedbooleanInitial state when nothing is saved, and for the server render. Default false (48px).
drawerOpenbooleanThe mobile drawer (controlled).
expandedbooleanExpanded (controlled).
idstring
linkComponentShellLinkComponentLink element for client navigation (Next.js `Link`, …). Default `<a>`.
messagesPartial<RookRailMessages>Interface text, for translation.
notificationsRookRailNotifications
onDrawerOpenChange((open: boolean) => void)
onExpandedChange((expanded: boolean) => void)
persistbooleanRemember the state in the `rook_rail` cookie, shared across Rook apps. Default true.
quickActionsRookRailItem[]Frequent actions above the expand toggle (New, Search). Listed first in the drawer.
spaceSwitcherRookRailSlotSpace switcher, under the workspace switcher.
supportfalse | RookRailSupportThe Help & support entry. `false` or missing hides it.
themeToggleRookRailSlotTheme toggle, above the account (and in the drawer footer).
workspaceSectionRookRailSectionThe Workspace block: workspace settings, members, billing and similar (usually links into Rook ID). Rendered after the sections, under its title, with Help & support directly above it.
workspaceSwitcherRookRailSlotWorkspace switcher, under the brand.

Variants

PropValuesDescription
expanded / defaultExpanded'false''true'48px icons (default) or 224px with labels (--mn-layout-rail-*).
brand.wordmark'node'Shown instead of the app icon while expanded and in the drawer.

The rail has one visual style: the product accent, so each app keeps its colour.

States

StateExposed asBehaviour
Collapsed / expandeddata-state="collapsed|expanded"On the root. Before hydration a persisted rail has no state and follows <html data-rook-rail> from the pre-paint script.
Active itemdata-active, aria-current="page"From active, or href matched against currentPath (prefix, or exact).
Badge–A count (99+ above 99) while expanded, a dot while collapsed; the count is always in the accessible name.
Help & support openaria-expanded="true", data-open, aria-controlsStyled like the active item while the support panel is open.
Focus–An on-accent focus ring inside the item.

Interaction behaviour

  • The expand button toggles labels, saves the choice for a year and tells every rail on the page; a change made in another Rook app is picked up when the tab regains focus.
  • Tooltips (label, shortcut, “Opens in Rook ID”) show on hover and focus only while collapsed.
  • External items open in a new tab with rel="noopener noreferrer"; internal links use linkComponent for client navigation.
  • Help & support sits directly above the Workspace block and calls support.onOpen.
  • The account button opens a menu: the person, Profile, extra entries, Rook account and Sign out.
  • Below 768px a top bar replaces the rail; its menu button opens the same navigation in a drawer that closes on Escape, on the backdrop and after choosing an item.

Keyboard support

KeysAction
Tab / Shift+TabEvery item, switcher and button is a normal Tab stop.
↓ / ↑Next / previous item in the rail or drawer (wraps).
Home / EndFirst / last item.
Enter / SpaceFollow a link, press a button, open a menu or switcher.
EscapeClose a tooltip, menu or the drawer; focus returns to what opened it.

Accessibility

Role
a navigation landmark ("Main", messages.nav) around the items; the drawer is a modal dialog named after the app
Accessible name
Items are named by their label (plus ", 3 unread" and "(opens in Rook ID in a new tab)"); icon-only controls keep visually hidden text.
  • The rail itself is not a complementary region (it avoids <aside>): only its navigation is a landmark, so it stays valid when embedded and never adds an unnamed region.
  • Section titles label their lists (aria-labelledby), also while visually hidden in the collapsed rail.
  • The expand button has aria-expanded and aria-controls pointing at the rail.
  • Help & support has aria-haspopup="dialog", aria-expanded and, while open, aria-controls.
  • The mobile drawer is a Monarch Sheet: focus moves in, is trapped, and returns to the menu button (the gap every audited copy had).
  • aria-keyshortcuts announces item shortcuts; the app implements the keys.
  • Forced-colours mode outlines the active item; motion is removed under reduced motion.

Responsive behaviour

  • 768px and up: a sticky full-height rail, 48px or 224px wide, whose middle scrolls on its own (overscroll-behavior: contain).
  • Below 768px: a 56px top bar with the menu button and the app mark; the drawer is 20rem wide, never wider than the screen, with 44px rows.
  • Slots get { collapsed, placement } and can switch to their compact form.

Theme support

  • The panel is the product accent (--mn-color-accent-solid with -on-solid text), so it follows the product, light, dark and more-contrast themes.
  • The drawer uses surface tokens, like other sheets.
  • Pair rookRailScript() with the tokens appearanceScript() in <head>.

Do and don't

Do

  • Render rookRailScript() in <head> and keep the default cookie, so the rail width is right on first paint and follows people between apps.
  • Pass the router’s link as linkComponent and the current path as currentPath.
  • Put workspace administration (settings, members, billing) in workspaceSection, so Help & support lands above it as in every app.
  • Give every item a short, unique label: it is the tooltip and the accessible name.

Don't

  • Do not fork the rail’s markup or style its internals from the app; ask for a slot or prop instead.
  • Do not hide important items only in the expanded state; the collapsed rail must offer every destination.
  • Do not use a badge for anything but a count people should act on.

Dependencies

KindDepends on
npm packagesreact
Monarch componentsAvatar, DropdownMenu, Kbd, Sheet, Tooltip

Testing coverage

  • Main navigation landmark; active item by path; brand home link.
  • Badges and external links in the accessible name; new-tab links are safe.
  • Workspace block under its heading with Help & support right above it; support button popup state and panel id.
  • Expand/collapse writes the rook_rail cookie (name, domain, max-age), mirrors <html data-rook-rail> and syncs other rails; starts from the saved state.
  • Controlled mode writes nothing; persist={false} keeps state locally.
  • Arrow keys, Home and End move focus; tooltips with label and shortcut while collapsed.
  • Slots get their context; account menu with profile, Rook account and Sign out.
  • Mobile drawer: focus moves in, closes after navigating and on Escape, focus returns.
  • Link component for internal links only; translated messages; server render and rookRailScript().
CheckCovered
Axe in jsdomYes
Server renderingYes
Browser suitesVisual baselines: /__gallery/rook-rail; Cross-browser axe: /components/rook-rail

Tests: packages/react/src/patterns/navigation/rook-rail