App Frame
A composition pattern for building the application frame with persistent sidebar navigation, a top bar, and scrollable content.
Most business applications share a common frame: a sidebar for navigation, a top bar for context and actions, and a scrollable content area. The App Frame pattern shows how to compose this frame using <AppShell>, <Page>, and <Page.Header>, covering content placement decisions, the heading outline, action hierarchy, and responsive strategy.
Looking for the component API?
For props, slot details, and accessibility, see the AppShell and Page (including Page.Header) component pages.
Structure
The app frame divides the screen into a top bar, the sidebar area, and the page. The diagram below shows the default arrangement with two-level navigation, and the code snippet shows how the components map to each area.
- Top Navigation: Stretches across the full width of the viewport. Its start slot holds the application logo, the panel toggle, and breadcrumbs for orientation. The end slot carries user-related controls such as account menus or status indicators.
- Rail: The narrow strip below the top bar with the app's top-level sections. It stays visible at all times, so switching sections is always one click away.
- Section panel: Shows the navigation of the active section next to the rail. Users can collapse it with the toggle to give the content more room. Rail and panel together are composed through
<Sidebar>. - Page: Takes up the remaining space. This is the scrollable content area. Wrap it in a
<Page>, which renders themainlandmark and owns the page's padding and rhythm. A page is a<Page.Header>followed by<Panel>s.
RouterProvider handles page navigation and AppShell positions everything on a CSS Grid. AppShell manages the sidebar's open/collapsed state internally, so the toggle in the header and the sidebar share state with no provider boilerplate.
<RouterProvider navigate={navigate}>
<AppShell>
<Sidebar>
<Sidebar.Rail current={path}>
<Sidebar.RailItem icon={<Ticket />} id="tickets">
Tickets
<Sidebar.Nav aria-label="Tickets">{/* Section items */}</Sidebar.Nav>
</Sidebar.RailItem>
{/* More sections and direct links */}
</Sidebar.Rail>
</Sidebar>
<TopNavigation>
<TopNavigation.Start>
{/* Logo */}
<Sidebar.Toggle variant="rail" />
{/* Breadcrumbs */}
</TopNavigation.Start>
<TopNavigation.End>{/* User menu */}</TopNavigation.End>
</TopNavigation>
<Page>
<Page.Header>
<Title>Billing</Title>
<Description>Manage your plan and invoices.</Description>
<Button variant="primary">Upgrade plan</Button>
</Page.Header>
<Panel>...</Panel>
<Panel>...</Panel>
</Page>
</AppShell>
</RouterProvider>The order of <Sidebar>, <TopNavigation>, and <Page> inside AppShell doesn't matter since the grid template handles positioning. For controlled sidebar state, render your own <Sidebar.Provider> around <AppShell>. See the AppShell docs.
Applications with flat navigation can swap the rail for a single-column <Sidebar>. The shell then switches to the sidebar-first arrangement. The sidebar runs the full viewport height and carries the logo in its header, while the top bar starts beside it. See Single-column sidebar for guidance on when the single column is the better fit.
Where things go
Each area has a clear purpose. Mixing responsibilities between them makes the layout harder to use and harder to maintain.
- Logo placement: In the default shell the full-width top bar is the brand anchor, so the logo lives in
TopNavigation.Startahead of<Sidebar.Toggle variant="rail" />, as shown in the example above. With a single-column sidebar, the sidebar takes over that role. Put the logo or application name in its header and keep the top navigation focused on orientation and utility actions. - Primary navigation: All section-level links belong in the sidebar. With two-level navigation the sections live on the rail and each section's links in its panel. In a single column, use groups (
<Sidebar.GroupLabel>) and separators (<Sidebar.Separator>) to organize them. See the Sidebar docs for drill-down navigation and detail page patterns. - Top navigation: Beyond the logo and the toggle, the top navigation exists primarily for breadcrumbs and search. In the default shell breadcrumbs follow the toggle in the start slot, and with a single-column sidebar they go in the middle slot. A search field can replace or complement them when global search is a core workflow. Don't place tabs, links, or secondary navigation here. That's what the sidebar is for. See the TopNavigation docs for further information.
- Breadcrumbs, not in the page header: Breadcrumbs describe where a page sits in the app, so they belong to the frame (which persists across navigation) rather than to
<Page.Header>, which describes the page itself. This is why Marigold places them in<TopNavigation>rather than the page header, unlike systems that fold breadcrumbs into the page title block. - Utility actions (top navigation, right side): The end of the top navigation is reserved for user-facing utilities like account menus, notification badges, or status indicators. Keep it concise. Too many items cause the navigation to overflow on smaller screens.
Do
Place the logo in the top navigation's start slot (with a single-column sidebar: in the sidebar header).
- Use breadcrumbs in the top navigation for page context.
- Keep header content concise so it adapts to different widths.
- Use the sidebar exclusively for navigation links and groups.
Don't
Don't place tabs or secondary navigation in the header middle slot.
Don't overload the top navigation with elements that cause horizontal scrolling.
Don't put action buttons, forms, or unrelated content in the sidebar.
Landmarks
Each app frame slot renders semantic HTML, so the screen reader's landmark list reflects the layout automatically.
| Slot | Landmark |
|---|---|
<TopNavigation> | banner |
<Sidebar> | complementary |
<Sidebar.Nav> | navigation |
<Sidebar.Rail> | complementary containing two navigation landmarks (rail and section panel) |
<Page> | main |
The <main> landmark is labelled by the page <h1> from <Page.Header>, so screen-reader landmark navigation announces the page by its title. When you compose multiple navigation regions inside the header (e.g. <TopNavigation.Middle> plus <TopNavigation.End>), give each a distinct aria-label so they can be told apart. Each <Panel> inside <Page> adds its own region landmark, labelled by its title.
F6 / Shift + F6 cycling is opt-in via the useLandmark hook. Today only <Drawer> opts in by default. Wrap the slots above (or any custom region) with useLandmark to add them to the cycle. See the Landmarks foundation for details.
Responsive behavior
<AppShell> always renders its grid and doesn't change at breakpoints. Responsive behavior comes from the components inside it. The sidebar collapses on desktop and becomes a modal sheet on mobile. With two-level navigation the drawer is the same single-column sheet — sections drill in, opened at the active section. The top navigation adapts its content at smaller widths. See the Sidebar and TopNavigation docs for details.
Page structure
Inside <Page>, lead with a <Page.Header> and follow it with the page's <Panel>s. <Page> supplies the vertical rhythm between them, so you don't wrap them in a <Stack> or <Inset>. When the rhythm between sections should differ from the header-to-content gap, wrap the sections in an optional <Page.Content> and set its space (see the Page docs).
<Page>
<Page.Header>
<Title>Billing</Title>
<Description>Manage your plan and invoices.</Description>
<Button variant="primary">Upgrade plan</Button>
</Page.Header>
<Panel>...</Panel>
<Panel>...</Panel>
</Page>The heading outline falls out of the defaults, with no per-component configuration:
| Element | Heading |
|---|---|
<Title> in <Page.Header> | h1 |
<Title> in <Panel.Header> | h2 |
<Title> in <Panel.Collapsible> | h3 |
<Page> does not enforce a max-width. For form-style pages, constrain width per surface with <Panel size="form"> rather than wrapping the whole page.
Common page layouts
Most application pages fall into a handful of shapes. Each is built from the same <Page> + <Panel> primitives. What differs is how many surfaces there are, how they are arranged, and how wide they run.
| Page | What it is | Recipe | Example |
|---|---|---|---|
| List / index | Browse and filter a collection before drilling in | A single full-width <Panel> wrapping a toolbar, filters, and a table | Venues |
| Detail / record | One object, with status and metadata | <Columns columns={[2, 1]}> with a primary column of content <Panel>s and a secondary "Overview" <Panel> for status (<Badge>) and key fields | User detail |
| Form | Create or edit a single thing | <Panel size="form"> surfaces (or a <Form>) constrained to a readable measure, with the primary action in the header or a submit button at the end | Event Form |
| Settings | Grouped configuration, often tabbed | <Tabs> below <Page.Header>, each panel a stack of <Panel size="form"> sections | Settings Form |
| Dashboard / overview | At-a-glance cards and summaries | <Page.Content> (or <Columns>) of full-width <Panel>s and <Card>s | Inventory |
The detail layout is the one most worth internalizing: a primary column carries the object's content, and a narrower secondary column carries its status and metadata. Because <Page.Header> has no slot for a status badge, the object's state lives in that secondary panel, not the title row.
When the Overview panel earns its place. Reach for it when the object carries a handful of facts worth scanning at a glance: its status, owner, key dates, counts, related records. It pays off on detail and record screens where the primary column runs long enough that a reader would otherwise lose track of the object's state while scrolling. Keep the split around two thirds primary to one third secondary, and let it collapse to a single column on narrow screens with <Columns collapseAt>, since a side column stacked below the fold goes unread on a phone.
When to skip it. A screen with only one or two facts does not need a second column. An Overview panel holding a lone badge reads as an empty rail, not a summary, so put those facts inline in the primary content or leave them out. And if a single piece of state is the whole reason someone opens the screen, that is a sign it is really a status view, not a record to read.
Actions hierarchy
Match the action to the level it belongs to. Promote a button only as high as its scope.
| Level | Where | Use |
|---|---|---|
| Page | <Page.Header> | <Button variant="primary"> for the page's primary action, <ActionMenu> for overflow |
| Panel | <Panel.Header> | <Button> (ghost chrome) or <ActionMenu> scoped to that surface |
| Row | inside a table/list row | <Button> / <ActionMenu> scoped to the item |
| Account | <TopNavigation.End> | account menu and notifications (global utilities, not page actions) |
A page's primary action is a prominent <Button variant="primary"> in the header, claiming the header's action slot. Don't put page actions in <TopNavigation>, which is for global utilities.
Tabs on pages
When a page splits into tabbed views, render <Tabs> as a sibling below <Page.Header>, not inside it. The header keeps the page title and primary action. The tabs switch the content beneath.
<Page>
<Page.Header>
<Title>Event settings</Title>
</Page.Header>
<Tabs>
<Tabs.List>...</Tabs.List>
<Tabs.Panel id="general">...</Tabs.Panel>
</Tabs>
</Page>Empty state
When a page (or a Panel within it) has no data yet, render an <EmptyState> in place of the missing content. Keep the <Page.Header> so the page title and primary action stay available. The empty state replaces the Panels, not the header.
Full demo
A complete app frame with sidebar navigation (including drill-down), breadcrumbs, user menu, and scrollable content.