Navigation layouts
The standard app shell: a fixed sidebar on the tray surface, a top bar with breadcrumbs and account menu, and the page content on the background. On narrow screens the sidebar moves into a Drawer behind a menu button.
Overview
When the preview is narrower than 520px, the sidebar collapses into a drawer behind the menu button.
<div className="shell">
<aside className="shell__sidebar">
<SidebarNav aria-label="Main">
<SidebarNav.Item as={RouterLink} href="/" icon={<Home {...ICON} />} active>Overview</SidebarNav.Item>
<SidebarNav.Item href="/inbox" icon={<Inbox {...ICON} />} trailing={<Badge>4</Badge>}>Inbox</SidebarNav.Item>
<SidebarNav.Group label="Pinned">…</SidebarNav.Group>
</SidebarNav>
</aside>
<div className="shell__main">
<header className="shell__topbar">
<Drawer.Root open={open} onOpenChange={setOpen}>
<Drawer.Trigger asChild><IconButton label="Open navigation" icon={<Menu {...ICON} />} /></Drawer.Trigger>
<Drawer.Content side="left" size="sm">…same SidebarNav…</Drawer.Content>
</Drawer.Root>
<Breadcrumb.Root>…</Breadcrumb.Root>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild><button aria-label="Account menu"><Avatar name="Ada Lovelace" size="sm" /></button></DropdownMenu.Trigger>
<DropdownMenu.Content placement="bottom-end">…</DropdownMenu.Content>
</DropdownMenu.Root>
</header>
<main className="shell__content">…</main>
</div>
</div>The CSS
The shell is layout, so it is yours: a few lines of grid using Merid's tokens.
.shell {
display: grid;
grid-template-columns: 240px minmax(0, 1fr);
min-height: 100dvh;
}
.shell__sidebar {
position: sticky;
top: 0;
height: 100dvh;
overflow-y: auto;
padding: var(--mrd-space-4) var(--mrd-space-3);
background: var(--mrd-tray);
border-inline-end: 1px solid var(--mrd-line);
}
.shell__topbar {
position: sticky;
top: 0;
z-index: var(--mrd-z-sticky);
display: flex;
align-items: center;
justify-content: space-between;
height: 52px;
padding-inline: var(--mrd-space-4);
background: var(--mrd-bg);
border-bottom: 1px solid var(--mrd-line);
}
.shell__content { padding: var(--mrd-space-8) var(--mrd-gutter); }
.shell__menu { display: none; }
@media (width < 720px) {
.shell { grid-template-columns: minmax(0, 1fr); }
.shell__sidebar { display: none; }
.shell__menu { display: inline-flex; }
}The preview uses a container query at 520px so it responds to the preview width, not the window; in an app, use the media query.
Guidelines
- One nav, two containers. Render the same
SidebarNavin the sidebar and in the drawer so they never drift. Close the drawer on navigation. - Active state comes from the route. Pass
activefrom the current path; it setsaria-current="page". With a router, pass its link asas— see Next.js and React Router and TanStack. - Group, don't nest. Use
SidebarNav.Grouplabels for sections. More than two levels of navigation belongs in the page (tabs), not the sidebar. - Top bar is for context and account, not primary navigation: breadcrumbs on the start side, notifications and the account menu on the end side.
- Landmarks. The sidebar
navis labelled byaria-label; wrap page content in<main>and add a "Skip to content" link as the first focusable element. - Icons at 16px with 1.75 stroke — see Icons.
Variations
- Top nav only: for apps with fewer than six sections, drop the sidebar and use
Tabsor links in the top bar. - Settings sub-nav: a second
SidebarNavinside the content area, as in Settings pages.