Marigold
v18.0.0-beta.4
Marigold
v18.0.0-beta.4

Application

MarigoldProvider
RouterProvider

Layout

AppShellbeta
Aside
Aspect
Center
Columns
Container
Grid
Inline
Inset
Pagebeta
Panelbeta
Scrollable
Split
Stack
Tiles

Actions

Buttonupdated
ButtonGroupbeta
Link
LinkButton
ToggleButtonbeta

Form

Autocomplete
Calendar
Checkbox
ComboBox
DateField
DatePicker
DateRangePickerbeta
FileField
Form
NumberField
Radio
RangeCalendaralpha
SearchField
SegmentedControlbeta
Select
SelectListupdated
Slider
Switchupdated
TagFieldbeta
TextArea
TextField
TimeField

Collection

Cardupdated
Table
Tag
ActionBaralpha

Navigation

Accordion
Breadcrumbs
Pagination
Sidebarbeta
Tabs
TopNavigationbeta

Overlay

ActionMenualpha
ContextualHelp
Dialog
Drawer
Menuupdated
Toastbeta
Tooltip

Content

Badge
Descriptionalpha
Divider
EmptyStatebeta
ErrorStatebeta
Headline
Keyboardbeta
List
Loader
SectionMessage
SVG
Text
TextValuealpha
Titlealpha

Formatters

DateFormat
NumericFormat

Hooks and Utils

cn
cva
extendTheme
parseFormData
useAsyncListData
useLandmark
useListData
useTheme
VisuallyHidden
Components

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. For applications with several top-level sections, a two-level navigation mode pairs a persistent icon rail with a section panel.

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.

HeaderGroup labelNavigationSidebarToggleItemSeparatorFooter
  • 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.

With two-level navigation, the sidebar splits into a narrow rail of top-level destinations and a panel showing the active section's navigation:

Rail itemRailPinned itemPanel titleItemSection panel
  • Rail: The persistent strip of top-level destinations. It never disappears. Collapsing only narrows it to icons.
  • Rail item: A stacked icon-and-label tile. With a nested Sidebar.Nav it is a section that fills the panel. With only an href it is a direct link.
  • Pinned item: A rail item declared inside Sidebar.Footer, pinned to the bottom of the rail (e.g. a help center).
  • Section panel: The active section's navigation, headed by the panel title. This is the part the toggle collapses.

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.

The selected theme does not has any options for"variant" and "size".
Acme Inc.
DashboardOrdersProducts
Settings
AccountBilling
All productsCategoriesInventory
jane@acme.com
PropertyTypeDescription
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.

Single-column navigation items are text-only. If your application needs icons alongside every navigation label, coordinate across all applications first to ensure consistency. The exception is two-level navigation, whose rail tiles are icon-first by design, so icon is required on Sidebar.RailItem.

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.

Do

Keep labels short. Lead with the most meaningful word, since users scan the beginning first. "Order history" beats "History of orders."

Don't

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 single-column 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 whole application is organized into sections that each own their own sub-navigation, use two-level navigation instead of deeper nesting.

My App
DashboardOrdersCustomers
Settings
ProfileNotificationsSecurity

Dashboard

Click "Settings" to see the drill-down panel.
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 [--ui-viewport-height:25rem]">          <Sidebar>            <Sidebar.Header>              <Text weight="bold">My App</Text>            </Sidebar.Header>            <Sidebar.Nav current={currentPath}>              <Sidebar.Item href="/dashboard">Dashboard</Sidebar.Item>              <Sidebar.Item href="/orders">Orders</Sidebar.Item>              <Sidebar.Item href="/customers">Customers</Sidebar.Item>              <Sidebar.Separator />              <Sidebar.Item id="settings" textValue="Settings">                Settings                <Sidebar.Item href="/settings/profile">Profile</Sidebar.Item>                <Sidebar.Item href="/settings/notifications">                  Notifications                </Sidebar.Item>                <Sidebar.Item href="/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>  );};

Two-level navigation

When an application has several top-level sections that each carry their own sub-navigation, like tickets, events, contacts, and settings, a single column starts to strain. Everything either nests behind drill-downs, or the list grows past what users can scan. Sidebar.Rail splits the navigation into two persistent levels instead: a narrow rail of icon-and-label tiles for the sections, and a panel beside it showing the active section's items. Both levels stay visible, so switching sections is one click and users never lose sight of where they are.

A Sidebar.RailItem wrapping a Sidebar.Nav is a section. Selecting it fills the panel with its navigation and lands on its first page. An item with only an href is a direct link that navigates immediately and shows no panel. Rail items declared inside Sidebar.Footer render pinned at the bottom of the rail.

TicketsEventsContactsReports
Help

Tickets

OpenUnassignedArchive
Logo
User Menu

Open tickets

The rail stays put; the panel shows the active section.

Overview

You are viewing Open tickets. Select another rail section to swap the panel, or collapse it with the toggle.
import { useState } from 'react';import {  AppShell,  Description,  Page,  Panel,  RouterProvider,  Sidebar,  Text,  Title,  TopNavigation,} from '@marigold/components';import {  BarChart3,  CalendarDays,  LifeBuoy,  Ticket,  Users,} from '@marigold/icons';import { DemoViewport } from '@/ui/DemoViewport';const pages: Record<string, string> = {  '/tickets/open': 'Open tickets',  '/tickets/unassigned': 'Unassigned',  '/tickets/archive': 'Archive',  '/events/upcoming': 'Upcoming events',  '/events/past': 'Past events',  '/contacts/people': 'People',  '/contacts/companies': 'Companies',  '/reports': 'Reports',  '/help': 'Help center',};export default () => {  const [path, setPath] = useState('/tickets/open');  const label = pages[path] ?? 'Page';  return (    <DemoViewport>      <RouterProvider navigate={setPath}>        <AppShell>          <Sidebar>            <Sidebar.Rail current={path}>              <Sidebar.RailItem icon={<Ticket />} id="tickets">                Tickets                <Sidebar.Nav aria-label="Tickets">                  <Sidebar.Item href="/tickets/open">Open</Sidebar.Item>                  <Sidebar.Item href="/tickets/unassigned">                    Unassigned                  </Sidebar.Item>                  <Sidebar.Item href="/tickets/archive">Archive</Sidebar.Item>                </Sidebar.Nav>              </Sidebar.RailItem>              <Sidebar.RailItem icon={<CalendarDays />} id="events">                Events                <Sidebar.Nav aria-label="Events">                  <Sidebar.Item href="/events/upcoming">Upcoming</Sidebar.Item>                  <Sidebar.Item href="/events/past">Past</Sidebar.Item>                </Sidebar.Nav>              </Sidebar.RailItem>              <Sidebar.RailItem icon={<Users />} id="contacts">                Contacts                <Sidebar.Nav aria-label="Contacts">                  <Sidebar.Item href="/contacts/people">People</Sidebar.Item>                  <Sidebar.Item href="/contacts/companies">                    Companies                  </Sidebar.Item>                </Sidebar.Nav>              </Sidebar.RailItem>              {/* An item without a nested nav is a direct link — no panel. */}              <Sidebar.RailItem icon={<BarChart3 />} href="/reports">                Reports              </Sidebar.RailItem>              {/* Rail items inside the footer render pinned at the bottom. */}              <Sidebar.Footer>                <Sidebar.RailItem icon={<LifeBuoy />} href="/help">                  Help                </Sidebar.RailItem>              </Sidebar.Footer>            </Sidebar.Rail>          </Sidebar>          <TopNavigation>            <TopNavigation.Start>              <Text weight="bold">Logo</Text>              <Sidebar.Toggle variant="rail" />            </TopNavigation.Start>            <TopNavigation.End>              <Text size="sm">User Menu</Text>            </TopNavigation.End>          </TopNavigation>          <Page>            <Page.Header>              <Title>{label}</Title>              <Description>                The rail stays put; the panel shows the active section.              </Description>            </Page.Header>            <Panel>              <Panel.Header>                <Title>Overview</Title>              </Panel.Header>              <Panel.Content>                <Text size="sm">                  You are viewing <strong>{label}</strong>. Select another rail                  section to swap the panel, or collapse it with the toggle.                </Text>              </Panel.Content>            </Panel>          </Page>        </AppShell>      </RouterProvider>    </DemoViewport>  );};

Collapsing behaves differently than in the single column: the toggle hides the panel while the rail narrows to an icon-only strip, so top-level navigation is always one click away. Re-selecting the active section reopens the panel. On small screens the rail renders as the same drawer as the single-column sidebar: one column where sections drill in (the drawer opens inside the active section), direct links are plain rows, and tapping a link closes the drawer.

The rail is paired with a top navigation that spans the full width of the screen, and AppShell switches to this layout automatically when it contains a rail. Place the brand and <Sidebar.Toggle variant="rail" /> in the top navigation's start slot.

Do

Use the rail for a small, stable set of sections (roughly 4–9) with recognizable icons. It is the app's permanent backbone, not a place for content that changes often.

Don't

Don't reach for the rail when your navigation fits one column. A flat list of pages needs no second level. The extra chrome only adds distance to every destination.

Active item

The sidebar marks the user's current page so they always know where they are. Pass the current pathname (typically from your router) to the current prop on Sidebar.Nav, and the matching item lights up automatically:

<Sidebar.Nav current={pathname}>
  <Sidebar.Item href="/overview">Overview</Sidebar.Item>
  <Sidebar.Item href="/analytics">Analytics</Sidebar.Item>
</Sidebar.Nav>

Matching is segment-aware, so dynamic routes like /users/abc-123 correctly highlight the /users item. When a leaf inside a drill-down branch matches, the parent branch lights up too and its sub-panel opens.

With two-level navigation, pass current to Sidebar.Rail instead. It resolves both levels at once: the matching page lights up in the panel and its section is marked on the rail.

For custom matching logic, pass a predicate (href, key) => boolean instead of a string. This is useful when URLs contain segments that don't map directly to item hrefs, such as a locale prefix:

<Sidebar.Nav
  current={(href, key) => {
    if (!href) return false;
    // strip locale prefix so /en/orders matches /orders
    return pathname.replace(/^\/(en|de)/, '') === href;
  }}
>
  <Sidebar.Item href="/orders">Orders</Sidebar.Item>
</Sidebar.Nav>

For escape-hatch cases (pinned items, items without an href), the per-item active prop on Sidebar.Item still works as a local override and stacks with the resolved match. The same override exists on Sidebar.RailItem: an active rail item wins over href matching and marks itself as the current destination.

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).

My App
DashboardOrdersCustomers
Orders
...
  1. Orders

Orders

Order
Customer
Total
ORD-4712
Anna Schmidt
€129.00
ORD-4713
Max Weber
€84.50
ORD-4714
Lena Fischer
€212.00
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 rowHeader>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 [--ui-viewport-height:25rem]">          <Sidebar>            <Sidebar.Header>              <Text weight="bold">My App</Text>            </Sidebar.Header>            <Sidebar.Nav current={currentPath}>              <Sidebar.Item href="/dashboard">Dashboard</Sidebar.Item>              <Sidebar.Item href="/orders">Orders</Sidebar.Item>              <Sidebar.Item href="/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

Don't add individual records as sidebar items. The sidebar points to the list. The list points to the detail.

Do

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.

With two-level navigation, the same toggle and shortcut collapse the section panel while the rail narrows to an icon-only strip, so top-level navigation never disappears.

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.

Two-level navigation exposes two navigation landmarks: the rail itself ("Primary navigation") and the active section's panel, named after its section. The current page announces aria-current="page" (on the panel leaf, or on a direct-link rail item), while its section ancestor on the rail announces aria-current="true". Rail tiles are plain, always-tabbable links. Arrow keys (and Home / End) also move between them as a shortcut. Switching sections moves focus to the panel heading so the swapped content is announced. Within the section panel the tab stop follows the current page: when the route changes, Tab re-enters the panel at the active item rather than wherever focus last rested. Collapsed, icon-only tiles reveal their label as a tooltip on hover or focus.

Props

Did you know? You can explore, test, and customize props live in Marigold's storybook. Watch the effects they have in real-time!
View Sidebar stories

Sidebar.Provider

Prop

Type

Sidebar

Prop

Type

Sidebar.Header

Prop

Type

Sidebar.Nav

Prop

Type

Accessibility props (1)

Prop

Type

Sidebar.Item

Prop

Type

Sidebar.GroupLabel

Prop

Type

Sidebar.Footer

Prop

Type

Sidebar.Rail

Prop

Type

Accessibility props (1)

Prop

Type

Sidebar.RailItem

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.
Last update: a day ago

Pagination

Component that divides up large datasets into manageable chunks.

Tabs

Component for building tabs.

On this page

AnatomyAppearanceUsageHeader and FooterOrganizing navigationDrill-down navigationTwo-level navigationActive itemDetail pagesCollapsingAccessibilityPropsSidebar.ProviderSidebarSidebar.HeaderSidebar.NavSidebar.ItemSidebar.GroupLabelSidebar.FooterSidebar.RailSidebar.RailItemAlternative components