Centrim Design System — a token-first, accessible React component system.
Navigation & page-level patterns
How the V2 navigation components combine into an application shell and a page: sidebar, top navigation, mobile drawer, breadcrumbs, tabs, page header, page sections and the filter bar. Navigation is declared once as data and rendered onto every surface.
What is this?
A single application shell plus a repeatable page structure. The shell owns global navigation; the page owns identity, related views and content.
When to use it
- Building any authenticated application screen
- Adding a new area to the product navigation
- Composing a page from a header, related views and sections
How to use it
- Declare destinations once as AppNavSection data
- Give the page one PageHeader and one Tabs group at most
- Group content with PageSection, refine it with FilterBar
What to avoid
- A second navigation system for the same purpose
- Tabs standing in for primary navigation
- Per-screen widths, spacing or active-state styling
Navigation differentiation
Each pattern answers one question. They are not interchangeable.
| Pattern | Answers | Use it for | Never use it for |
|---|---|---|---|
| Sidebar | Where can I go? | Primary application areas | Actions or view switching |
| Top navigation | What is global? | Brand, search, notifications, account | Deep hierarchy |
| Breadcrumb | Where am I? | Location within a hierarchy | Primary navigation |
| Tabs | Which view of this? | Related views of one record or list | Moving between app areas |
| Page header | What is this page? | Title, description, primary action | Filtering content |
| Filter bar | Which subset? | Search, filters, active filter chips | Navigating away |
Why it is designed this way
Navigation is declared as data, once. The shell renders the same destinations into the sidebar, the collapsed rail, the header links and the mobile drawer, so the active state and permission gating can never disagree between surfaces.
Application shell
Top navigation, collapsible sidebar, mobile drawer and exactly one main region.
AppShell
The V2 application chrome. Declare navigation as data; the shell renders it to every surface and owns the responsive behaviour.
import { AppShell, type AppNavSection } from "@/design-system";
When to use
- Every authenticated application screen
- Whenever navigation must stay identical across breakpoints
- When sections are permission-gated
When not to use
- Marketing or unauthenticated pages with no app navigation
- Inside a dialog or drawer
Residents — complete shell
Collapse the sidebar from its footer control; resize the preview to see the header links collapse into the drawer trigger.
Residents
Manage residents and their information.
All residents
Search, filter and open a resident record.
| Resident | Room | Service | Status |
|---|---|---|---|
| Amelia Hart | A-104 | Personal care | |
| Ravi Menon | B-212 | Day support | |
| Grace Whitfield | C-006 | Respite | |
| Tomas Berg | A-118 | Personal care |
Navigation data
const NAV: AppNavSection[] = [
{ items: [
{ id: "dashboard", label: "Dashboard", icon: <LayoutDashboard aria-hidden /> },
{ id: "residents", label: "Residents", icon: <Users aria-hidden />,
children: [{ id: "residents-active", label: "Active" }] },
{ id: "reports", label: "Reports", icon: <BarChart3 aria-hidden />,
visible: can("reports.read") },
] },
];Page structure
One header, one optional tab group, then sections.
PageSection
One band of page content: title, description, actions and body. Structural only — no surface, so it can hold a table, a card grid or a form without stacking containers.
import { PageSection } from "@/design-system";
When to use
- Grouping related content inside a page
- Giving a block of content its own heading and action
When not to use
- As a card — use Card when the content needs a surface
- For page identity — that is PageHeader
Section with an action
Recent activity
Latest activity across residents.
Activity list, table or card grid goes here.
Responsive & keyboard behaviour
The same navigation data, three widths.
| Surface | Desktop (lg+) | Tablet (md) | Mobile (<md) |
|---|---|---|---|
| Sidebar | Inline column, expanded or icon rail | Hidden — drawer | Hidden — drawer |
| Header links | Visible | Visible | Hidden — drawer |
| Global search | In the header | In the header | Hidden — page-level search |
| Filters | Inline filter bar | Inline filter bar | Filter toggle / drawer |
| Tabs | Inline | Inline | Horizontally scrollable |
| Context | Keys | Result |
|---|---|---|
| Navigation | Tab / Shift+Tab | Move between interactive elements in reading order |
| Sidebar branch | Enter / Space | Expand or collapse nested destinations |
| Tabs | Arrow keys | Move between tabs; Enter/Space activates |
| Menus | Arrow keys, Home/End | Navigate items; Escape closes |
| Mobile drawer | Escape | Closes and restores focus to the trigger |
Do
- Declare destinations once and pass them to AppShell.
- Hide destinations the user cannot access with `visible: false`.
- Keep one PageHeader and at most one tab group per page.
- Use Container for measure: standard content, wide data pages, full-width tables.
- Reuse the existing loading, empty, no-results and error states for page-level feedback.
Don't
- Don't build a second sidebar, drawer, menu or tooltip.
- Don't use tabs as primary navigation, or breadcrumbs as a menu.
- Don't style the active state per page, or signal it with colour alone.
- Don't nest scrolling regions unless the content genuinely requires it.
- Don't invent page widths, spacing values, breakpoints or z-index layers.