The sidebar is collapsed. Click the toggle icon in the page header to expand it.
import {
SidebarProvider,
SideNavigation,
SidebarPanel,
SidebarPanelHeader,
SidebarPanelContent,
SidebarPanelFooter,
SidebarPanelTitle,
SidebarItem,
SidebarSubItem,
useSidebar,
PageSection,
} from '@thrivecart/ui';
import type { NavigationItem } from '@thrivecart/ui';These examples show the complete sidebar system as used in production.
The standard app shell: SidebarProvider wrapping SideNavigation + PageSection + content area.
SidebarPanel with PageSection and content area
<SidebarProvider mainNavItems={[]} config={{ defaultSecondaryOpen: true }}>
<div className="flex h-[400px] w-full border border-secondary overflow-hidden">
<SidebarPanel>
<SidebarPanelHeader>
<SidebarPanelTitle>Navigation</SidebarPanelTitle>
</SidebarPanelHeader>
<SidebarPanelContent>
<SidebarItem
label="Dashboard"
iconOutlined={MdOutlineDashboard}
iconFilled={MdDashboard}
isActive
/>
<SidebarItem label="Contacts" iconOutlined={MdOutlinePeople} iconFilled={MdPeople}>
<SidebarSubItem label="All Contacts" />
<SidebarSubItem label="Segments" />
</SidebarItem>
<SidebarItem label="Campaigns" iconOutlined={MdOutlineCampaign} iconFilled={MdCampaign}>
<SidebarSubItem label="All Campaigns" />
<SidebarSubItem label="Templates" />
</SidebarItem>
<SidebarItem
label="Settings"
iconOutlined={MdOutlineSettings}
iconFilled={MdSettings}
/>
</SidebarPanelContent>
</SidebarPanel>
<div className="flex-1 flex flex-col overflow-hidden">
<PageSection pageTitle="Dashboard" showSidebarToggle />
<main className="flex-1 p-6 overflow-y-auto"></main>
</div>
</div>
</SidebarProvider>Sidebar with PageSection breadcrumbs and action buttons
<SidebarProvider mainNavItems={[]} config={{ defaultSecondaryOpen: true }}>
<div className="flex h-screen">
<SidebarPanel>
<SidebarPanelHeader>
<SidebarPanelTitle>Navigation</SidebarPanelTitle>
</SidebarPanelHeader>
<SidebarPanelContent>
<SidebarItem label="Dashboard" iconOutlined={MdOutlineDashboard} iconFilled={MdDashboard} />
<SidebarItem label="Campaigns" iconOutlined={MdOutlineCampaign} iconFilled={MdCampaign} isActive>
<SidebarSubItem label="All Campaigns" />
<SidebarSubItem label="Email Campaigns" isActive />
</SidebarItem>
<SidebarItem label="Contacts" iconOutlined={MdOutlinePeople} iconFilled={MdPeople} />
</SidebarPanelContent>
</SidebarPanel>
<div className="flex-1 flex flex-col">
<PageSection
breadcrumbs={[
{ label: 'Campaigns', href: '/campaigns' },
{ label: 'Email Campaigns', href: '/campaigns/email' },
{ label: 'Summer Sale' },
]}
showSidebarToggle
primaryAction={{ label: 'Save Changes' }}
secondaryActions={[{ label: 'Preview' }]}
/>
<main className="flex-1 p-6 overflow-y-auto">{children}</main>
</div>
</div>
</SidebarProvider>When the secondary panel is collapsed, PageSection automatically shows a toggle button so users can re-open it.
Secondary panel collapsed — toggle visible in PageSection
The sidebar is collapsed. Click the toggle icon in the page header to expand it.
<SidebarProvider mainNavItems={[]} config={{ defaultSecondaryOpen: false }}>
<div className="flex h-[300px] w-full border border-secondary overflow-hidden">
<SidebarPanel>
<SidebarPanelHeader>
<SidebarPanelTitle>Navigation</SidebarPanelTitle>
</SidebarPanelHeader>
<SidebarPanelContent>
<SidebarItem
label="Dashboard"
iconOutlined={MdOutlineDashboard}
iconFilled={MdDashboard}
isActive
/>
<SidebarItem label="Contacts" iconOutlined={MdOutlinePeople} iconFilled={MdPeople} />
</SidebarPanelContent>
</SidebarPanel>
<div className="flex-1 flex flex-col overflow-hidden">
<PageSection pageTitle="Dashboard" showSidebarToggle />
<main className="flex-1 p-6 overflow-y-auto">
<p className="text-ink text-sm">
The sidebar is collapsed. Click the toggle icon in the page header to expand it.
</p>
</main>
</div>
</div>
</SidebarProvider>Use PageSectionWizard instead of PageSection for multi-step flows.
Step indicators in the page header alongside sidebar navigation
<SidebarProvider mainNavItems={[]} config={{ defaultSecondaryOpen: true }}>
<div className="flex h-[400px] w-full border border-secondary overflow-hidden">
<SidebarPanel>
<SidebarPanelHeader>
<SidebarPanelTitle>Navigation</SidebarPanelTitle>
</SidebarPanelHeader>
<SidebarPanelContent>
<SidebarItem
label="Dashboard"
iconOutlined={MdOutlineDashboard}
iconFilled={MdDashboard}
/>
<SidebarItem
label="Campaigns"
iconOutlined={MdOutlineCampaign}
iconFilled={MdCampaign}
isActive
>
<SidebarSubItem label="Templates" />
<SidebarSubItem label="All Campaigns" isActive />
</SidebarItem>
</SidebarPanelContent>
</SidebarPanel>
<div className="flex-1 flex flex-col overflow-hidden">
<PageSectionWizard
title="Create Campaign"
showSidebarToggle
currentStep={1}
totalSteps={4}
steps={[
{ id: 'details', label: 'Details' },
{ id: 'content', label: 'Content' },
{ id: 'audience', label: 'Audience' },
{ id: 'review', label: 'Review' },
]}
/>
<main className="flex-1 overflow-y-auto px-space-2xl">
<div className="max-w-2xl">
<h3 className="font-medium mb-4">Email Content</h3>
<div className="space-y-4">
<div className="h-8 bg-bg rounded w-full" />
<div className="h-32 bg-bg rounded w-full" />
</div>
</div>
</main>
</div>
</div>
</SidebarProvider>SidebarItem supports iconOutlined (default) and iconFilled (active/hover). The icon swaps automatically.
Icons swap between outlined and filled on active state
<SidebarItem
label="Dashboard"
iconOutlined={MdOutlineDashboard}
iconFilled={MdDashboard}
isActive={true}
/>The useSidebar() hook provides access to all sidebar state and actions from any component inside a SidebarProvider.
'use client';
import { useSidebar } from '@thrivecart/ui';
function MyComponent() {
const {
// State
secondaryIsOpen, // boolean — is the panel open?
activeMainItem, // string | null — active primary nav item ID
activeSecondaryItem, // string | null — active secondary nav item ID
activeSubItem, // string | null — active sub-item ID
expandedSecondaryItems, // string[] — IDs of expanded accordion items
mainNavItems, // NavigationItem[] — all navigation items
config, // SidebarConfig — current configuration
// Actions
setSecondaryIsOpen, // (open: boolean) => void
toggleSecondary, // () => void
setActiveMainItem, // (id: string) => void — resets secondary + sub
setActiveSecondaryItem, // (id: string) => void
setActiveSubItem, // (id: string | null) => void
toggleSecondaryExpanded, // (id: string) => void — single accordion behavior
setExpandedSecondaryItems, // (ids: string[]) => void
} = useSidebar();
return (
<div>
<p>Sidebar is {secondaryIsOpen ? 'open' : 'closed'}</p>
<p>Active: {activeMainItem}</p>
<button onClick={toggleSecondary}>Toggle Sidebar</button>
</div>
);
}Components like PageSection and PageSectionWizard use useSidebar internally to show a toggle button when the sidebar is collapsed:
// Inside PageSection (simplified)
function PageSection({ showSidebarToggle, ...props }) {
const { secondaryIsOpen, toggleSecondary } = useSidebar();
return (
<header>
{showSidebarToggle && !secondaryIsOpen && (
<IconButton
icon={<MdOutlineChromeReaderMode />}
onClick={toggleSecondary}
aria-label="Open sidebar"
/>
)}
{/* ... breadcrumbs and actions */}
</header>
);
}You can build custom components that respond to sidebar state the same way:
// Custom breadcrumb that adjusts when sidebar closes
function AdaptiveBreadcrumb() {
const { secondaryIsOpen } = useSidebar();
return (
<nav className={secondaryIsOpen ? 'pl-0' : 'pl-4'}>
{/* breadcrumb items */}
</nav>
);
}SidebarProvider wraps your layout and provides state to all sidebar components via React Context.
import { SidebarProvider } from '@thrivecart/ui';
import type { NavigationItem } from '@thrivecart/ui';
<SidebarProvider
mainNavItems={navItems} // Required: navigation tree
config={{
defaultMainItem: 'dashboard', // Initially active item
defaultSecondaryOpen: true, // Panel starts open
animationDuration: 300, // Transition duration (ms)
persistState: true, // Save open/closed to cookie
}}
>
{children}
</SidebarProvider>When persistState: true, the sidebar writes its open/closed state to a cookie. Read it server-side for SSR:
// layout.tsx (Server Component)
import { cookies } from 'next/headers';
import { SIDEBAR_COOKIE_NAME, SidebarProvider } from '@thrivecart/ui';
export default async function Layout({ children }) {
const cookieStore = await cookies();
const sidebarOpen = cookieStore.get(SIDEBAR_COOKIE_NAME)?.value !== 'false';
return (
<SidebarProvider
mainNavItems={navItems}
config={{ defaultSecondaryOpen: sidebarOpen, persistState: true }}
>
{children}
</SidebarProvider>
);
}The cookie sidebar_state is set with a 7-day max-age and updates on every toggle.
interface NavigationItem {
id: string;
label: string;
icon: ElementType | string; // Primary rail icon (filled)
iconOutlined?: ElementType | string; // Primary rail icon (default)
iconDuotone?: ElementType | string; // Primary rail icon (active)
onClick: () => void;
children?: SecondaryNavigationItem[];
}
interface SecondaryNavigationItem {
id: string;
label: string;
iconOutlined?: ElementType | string;
iconFilled?: ElementType | string;
onClick: () => void;
subItems?: SubNavigationItem[];
}
interface SubNavigationItem {
id: string;
label: string;
onClick: () => void;
}mobileMode="hidden" to completely hide the panel on mobile instead of using a Sheet<SidebarPanel mobileMode="sheet" /> {/* Default: Sheet overlay on mobile */}
<SidebarPanel mobileMode="hidden" /> {/* Hide completely on mobile */}For simpler setups without a provider, use the useSideBar Zustand store. It provides the same state globally without React Context.
import { useSideBar } from '@thrivecart/ui';
function Navigation() {
const { secondaryIsOpen, toggleSecondary, activeMainItem, setActiveMainItem } = useSideBar();
// Same API, no provider needed
}There is also useSideBarState which uses plain React useState — useful for isolated components.
SidebarPanel, SidebarPanelTitle, PageSection, and PageSectionWizard all depend on the SidebarProvider context. Without it they will throw or lose functionality.
When the sidebar is collapsed, users need a way to re-open it. The toggle button in PageSection handles this.
Set persistState: true and read the cookie server-side so the sidebar state survives page reloads.
The icon swap on hover/active gives clear visual feedback. Use Material Design outlined + filled pairs.
Primary rail > Secondary item > Sub-item is the maximum. Deeper nesting is confusing.
Use SidebarPanelFooter for secondary actions (Help, Settings). Don't leave the bottom of the panel empty.
| Prop | Type | Default | Description |
|---|---|---|---|
mainNavItems | NavigationItem[] | — | Navigation tree (required) |
config.defaultMainItem | string | First item ID | Initially active main item |
config.defaultSecondaryOpen | boolean | true | Whether panel starts open |
config.animationDuration | number | 300 | Animation duration (ms) |
config.persistState | boolean | false | Write state to cookie |
| Prop | Type | Default | Description |
|---|---|---|---|
width | number | 224 | Panel width when open (px) |
animationDuration | number | 300 | Transition duration (ms) |
mobileMode | 'sheet' | 'hidden' | 'sheet' | Mobile behavior |
className | string | — | Additional className |
children | ReactNode | — | Header, content, footer |
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Title text |
hideToggle | boolean | false | Hide collapse/expand toggle |
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Item label |
iconOutlined | ElementType | string | — | Default icon |
iconFilled | ElementType | string | — | Active/hover icon |
isActive | boolean | false | Active state |
onClick | () => void | — | Click handler |
children | ReactNode | — | Nested SidebarSubItems |
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Sub-item label |
isActive | boolean | false | Active state |
onClick | () => void | — | Click handler |
Navigation Landmarks
SidebarPanel renders a nav element with aria-label="Secondary navigation", making it a recognized navigation landmark for screen readers. The primary rail has a separate nav with aria-label="Main navigation".
<nav> landmarkaria-expandedaria-current="page" or visual indicatorsaria-label ("Collapse sidebar" / "Expand sidebar")prefers-reduced-motion