Sidebar
Persistent navigation panel for organizing app-level navigation.
The <Sidebar> provides persistent, app-level navigation placed on the left side of the screen. It organizes navigation items into a vertical list, supports drill-down sub-navigation for nested sections, and adapts between a desktop panel and a mobile sheet overlay.
Anatomy
A sidebar consists of a panel alongside the main content area. The panel is organized into three zones: a sticky header, a scrollable navigation area, and a sticky footer.
- Header: Sticky top area for branding, logos, or workspace switchers.
- Group label: A non-interactive section heading to organize items into logical groups.
- Navigation: Scrollable area containing navigation items with drill-down support.
- Sidebar: The panel itself, with header, navigation, and footer zones.
- Toggle: A button placed outside the sidebar to open or close it.
- Item: A navigation entry that is either a link or a branch that opens a sub-panel.
- Separator: A visual divider between groups of items.
- Footer: Sticky bottom area for user profiles, settings, or secondary actions.
Appearance
The appearance of a component can be customized using the variant and size props. These props adjust the visual style and dimensions of the component, available values are based on the active theme.
| Property | Type | Description |
|---|---|---|
variant | - | The available variants of this component. |
size | - | The available sizes of this component. |
Usage
The sidebar is the central navigation of every application. It is part of the default app layout and how users move between sections. It sits on the left side of the screen and is organized into a header for branding, a scrollable navigation area, and a footer for secondary actions. On mobile, it adapts to a sheet overlay.
In most applications, the sidebar is paired with a top navigation to form an L-shaped app shell. The top navigation handles global actions like search, breadcrumbs, and user menus, while the sidebar handles section-level navigation.
The sidebar does not have dedicated icon support yet. If your application needs icons alongside navigation labels, coordinate across all applications first to ensure consistency. For now, use text-only labels.
Header and Footer
Above and below the scrollable navigation sit two sticky areas: the header and the footer. They stay visible while users scroll through nav items, but neither is meant for navigation itself.
Put your branding in the header. The app logo, the product name, or both. It tells users where they are and that is all it should do. When using a top navigation, place the application logo in the sidebar header, not in the top navigation bar.
Secondary actions go in the footer. A link to help or support, a settings shortcut, or a way to send feedback. Things users reach for occasionally, not on every visit.
Organizing navigation
Think about what your users need to get to most often and put those items at the top. People scan vertical lists from top to bottom, so the order matters.
A common mistake is to sort items alphabetically or mirror the internal system structure. Both ignore how people actually work. An "Analytics" link sorted to the top alphabetically is unhelpful if users open it once a month, while "Orders", the thing they check ten times a day, is buried further down. Order by frequency of use, not by the alphabet or your org chart.
When the list gets long, break it into groups using section headings (<Sidebar.GroupLabel>) and visual dividers (<Sidebar.Separator>). Group items by what the user is trying to do, not by how your system is structured internally. Avoid putting user-generated or unbounded content in the navigation, since lists that can grow without limit make the sidebar hard to use.
If users need to see all items at a glance, groups with headings are the right choice. If a section has its own sub-pages that users only visit occasionally, nest them behind a branch item instead. See drill-down navigation for more on nesting.
Keep labels short. Lead with the most meaningful word, since users scan the beginning first. "Order history" beats "History of orders."
Don't use long, technical labels like "Transaction management module" when a simple word like "Orders" works.
Drill-down navigation
Use drill-down when a group of sub-pages belongs together under one parent, like "Settings" with sub-pages for profile, notifications, and security. Clicking a branch item opens a sub-panel with a back button to return.
Don't use drill-down just to shorten a long list. Every branch adds a click, so it should only be used when the sub-pages form a clear group.
The sidebar supports one level of nesting. More levels would force users to remember where they are in a deep hierarchy, and navigating back through multiple levels gets tedious fast. If your content needs more depth, reconsider how you organize your sections rather than adding more nesting.
Dashboard
import { useState } from 'react';import { Headline, Sidebar, Text } from '@marigold/components';import { RouterProvider } from '@marigold/components';export default () => { const [currentPath, setCurrentPath] = useState('/dashboard'); return ( <RouterProvider navigate={setCurrentPath}> <Sidebar.Provider> <div className="flex h-100"> <Sidebar> <Sidebar.Header> <Text weight="bold">My App</Text> </Sidebar.Header> <Sidebar.Nav> <Sidebar.Item href="/dashboard" active={currentPath === '/dashboard'} > Dashboard </Sidebar.Item> <Sidebar.Item href="/orders" active={currentPath === '/orders'}> Orders </Sidebar.Item> <Sidebar.Item href="/customers" active={currentPath === '/customers'} > Customers </Sidebar.Item> <Sidebar.Separator /> <Sidebar.Item id="settings" textValue="Settings"> Settings <Sidebar.Item href="/settings/profile" active={currentPath === '/settings/profile'} > Profile </Sidebar.Item> <Sidebar.Item href="/settings/notifications" active={currentPath === '/settings/notifications'} > Notifications </Sidebar.Item> <Sidebar.Item href="/settings/security" active={currentPath === '/settings/security'} > Security </Sidebar.Item> </Sidebar.Item> </Sidebar.Nav> </Sidebar> <main className="flex-1 p-4"> <Sidebar.Toggle /> <Headline level={2}> {currentPath .replace(/^\/settings\//, '') .replace('/', '') .replace(/-/g, ' ') .replace(/^\w/, c => c.toUpperCase())} </Headline> <Text>Click "Settings" to see the drill-down panel.</Text> </main> </div> </Sidebar.Provider> </RouterProvider> );};Detail pages
The sidebar shows sections and categories, not individual records. When a user opens a specific order from the "Orders" page, the detail view fills the main content area. The sidebar does not change. "Orders" stays highlighted so the user can tell which section they are in.
Do not add individual records as sidebar items. A sidebar that lists every order or ticket grows without limit and becomes unusable. The sidebar points to the list; the list points to the detail.
For hierarchies deeper than two levels, pair the sidebar with breadcrumbs. The sidebar lets users move between sections, breadcrumbs show the path back (e.g. Orders > ORD-4712).
import { useState } from 'react';import { Breadcrumbs, Headline, Inline, Link, NumericFormat, RouterProvider, Sidebar, Stack, Table, Text,} from '@marigold/components';const orders = [ { id: 'ORD-4712', customer: 'Anna Schmidt', total: 129 }, { id: 'ORD-4713', customer: 'Max Weber', total: 84.5 }, { id: 'ORD-4714', customer: 'Lena Fischer', total: 212 },];const pages: Record<string, string> = { '/dashboard': 'Dashboard', '/orders': 'Orders', '/customers': 'Customers',};const OrderList = () => ( <Stack space={4}> <Headline level={2}>Orders</Headline> <Table aria-label="Orders" selectionMode="none"> <Table.Header> <Table.Column>Order</Table.Column> <Table.Column>Customer</Table.Column> <Table.Column>Total</Table.Column> </Table.Header> <Table.Body> {orders.map(order => ( <Table.Row key={order.id}> <Table.Cell> <Link href={`/orders/${order.id}`}>{order.id}</Link> </Table.Cell> <Table.Cell>{order.customer}</Table.Cell> <Table.Cell> <NumericFormat value={order.total} style="currency" currency="EUR" /> </Table.Cell> </Table.Row> ))} </Table.Body> </Table> </Stack>);const OrderDetail = ({ id }: { id: string }) => { const order = orders.find(o => o.id === id); if (!order) return null; return ( <Stack space={4}> <Headline level={2}>{order.id}</Headline> <Text> Customer: {order.customer} <br /> Total:{' '} <NumericFormat value={order.total} style="currency" currency="EUR" /> </Text> </Stack> );};export default () => { const [currentPath, setCurrentPath] = useState('/orders'); const orderMatch = currentPath.match(/^\/orders\/(.+)$/); const selectedOrder = orderMatch?.[1] ?? null; const activePath = selectedOrder ? '/orders' : currentPath; return ( <RouterProvider navigate={setCurrentPath}> <Sidebar.Provider> <div className="flex h-100"> <Sidebar> <Sidebar.Header> <Text weight="bold">My App</Text> </Sidebar.Header> <Sidebar.Nav> <Sidebar.Item href="/dashboard" active={activePath === '/dashboard'} > Dashboard </Sidebar.Item> <Sidebar.Item href="/orders" active={activePath === '/orders'}> Orders </Sidebar.Item> <Sidebar.Item href="/customers" active={activePath === '/customers'} > Customers </Sidebar.Item> </Sidebar.Nav> </Sidebar> <main className="grid flex-1 grid-rows-[auto_1fr] gap-8 overflow-auto pl-4"> <Inline alignY="center"> <Sidebar.Toggle /> <Breadcrumbs> <Breadcrumbs.Item href={activePath}> {pages[activePath] ?? activePath} </Breadcrumbs.Item> {selectedOrder && ( <Breadcrumbs.Item href={`/orders/${selectedOrder}`}> {selectedOrder} </Breadcrumbs.Item> )} </Breadcrumbs> </Inline> {activePath === '/orders' && !selectedOrder && <OrderList />} {activePath === '/orders' && selectedOrder && ( <OrderDetail id={selectedOrder} /> )} {activePath === '/dashboard' && ( <Headline level={2}>Dashboard</Headline> )} {activePath === '/customers' && ( <Headline level={2}>Customers</Headline> )} </main> </div> </Sidebar.Provider> </RouterProvider> );};Don't add individual records as sidebar items. The sidebar points to the list; the list points to the detail.
For hierarchies deeper than two levels, pair the sidebar with breadcrumbs to show the path back.
Collapsing
Users can collapse or expand the sidebar with the toggle button or the keyboard shortcut Cmd+B / Ctrl+B. The sidebar remembers this preference across page loads. When combined with a top navigation, the sidebar toggle button is typically placed in the top navigation's start slot.
Accessibility
The sidebar renders as a <nav> element so screen readers can identify it as a navigation landmark. The keyboard shortcut Cmd+B / Ctrl+B toggles it globally. When drilling into a sub-panel, focus moves to the back button, and returns to the branch trigger when navigating back. The active item is announced as the current page. On mobile, the overlay traps focus and can be dismissed with Escape. Built-in strings support German and English localization.
Props
Sidebar.Provider
Prop
Type
Sidebar
Prop
Type
Sidebar.Header
Prop
Type
Sidebar.Nav
Sidebar.Item
Prop
Type
Sidebar.GroupLabel
Prop
Type
Sidebar.Footer
Prop
Type
Alternative components
- Tabs: Use when content groups are at the same hierarchy level and users switch between views within a single page, not across the application.
- Accordion: Use for collapsing content sections within a page, not for app-level navigation.
- Breadcrumbs: Use to show the user's location within a hierarchy. Breadcrumbs complement a sidebar but don't replace it.
- Top Navigation: Use for the horizontal top bar that houses global actions like search, breadcrumbs, and user menus. Pair it with a sidebar for the standard L-shaped app shell.