Thrive Design System

Components

SidebarPanel

A collapsible sidebar navigation system with provider-based state management, nested items, cookie persistence, and responsive mobile support.

Usage

import {
  SidebarProvider,
  SideNavigation,
  SidebarPanel,
  SidebarPanelHeader,
  SidebarPanelContent,
  SidebarPanelFooter,
  SidebarPanelTitle,
  SidebarItem,
  SidebarSubItem,
  useSidebar,
  PageSection,
} from '@thrivecart/ui';
import type { NavigationItem } from '@thrivecart/ui';

Full Layout Examples

These examples show the complete sidebar system as used in production.

Dashboard Layout

The standard app shell: SidebarProvider wrapping SideNavigation + PageSection + content area.

Full App Shell

SidebarPanel with PageSection and content area

Dashboard

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

With Breadcrumbs and Actions

Breadcrumb Navigation

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>

Collapsed Sidebar

When the secondary panel is collapsed, PageSection automatically shows a toggle button so users can re-open it.

Collapsed State

Secondary panel collapsed — toggle visible in PageSection

Dashboard

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>

Wizard Layout

Use PageSectionWizard instead of PageSection for multi-step flows.

Wizard with Sidebar

Step indicators in the page header alongside sidebar navigation

Create Campaign

Email Content

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

Icon States

SidebarItem supports iconOutlined (default) and iconFilled (active/hover). The icon swaps automatically.

Outlined / Filled Icons

Icons swap between outlined and filled on active state

Dashboard

<SidebarItem
label="Dashboard"
iconOutlined={MdOutlineDashboard}
iconFilled={MdDashboard}
isActive={true}
/>

useSidebar Hook

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>
  );
}

Real-World Usage

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

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;
}

Mobile Behavior

  • Desktop (768px+): Panel animates width open/closed with a smooth transition
  • Mobile (below 768px): Panel content renders inside a Sheet (slide-in overlay)
  • Set 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 */}

useSideBar (Zustand Alternative)

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.

Best Practices

Always wrap with SidebarProvider

SidebarPanel, SidebarPanelTitle, PageSection, and PageSectionWizard all depend on the SidebarProvider context. Without it they will throw or lose functionality.

Set showSidebarToggle on PageSection

When the sidebar is collapsed, users need a way to re-open it. The toggle button in PageSection handles this.

Persist sidebar state with cookies

Set persistState: true and read the cookie server-side so the sidebar state survives page reloads.

Use outlined/filled icon pairs

The icon swap on hover/active gives clear visual feedback. Use Material Design outlined + filled pairs.

Don't nest more than 3 levels

Primary rail > Secondary item > Sub-item is the maximum. Deeper nesting is confusing.

Don't forget the footer

Use SidebarPanelFooter for secondary actions (Help, Settings). Don't leave the bottom of the panel empty.

Props

SidebarProvider

PropTypeDefaultDescription
mainNavItemsNavigationItem[]Navigation tree (required)
config.defaultMainItemstringFirst item IDInitially active main item
config.defaultSecondaryOpenbooleantrueWhether panel starts open
config.animationDurationnumber300Animation duration (ms)
config.persistStatebooleanfalseWrite state to cookie

SidebarPanel

PropTypeDefaultDescription
widthnumber224Panel width when open (px)
animationDurationnumber300Transition duration (ms)
mobileMode'sheet' | 'hidden''sheet'Mobile behavior
classNamestringAdditional className
childrenReactNodeHeader, content, footer

SidebarPanelTitle

PropTypeDefaultDescription
childrenReactNodeTitle text
hideTogglebooleanfalseHide collapse/expand toggle

SidebarItem

PropTypeDefaultDescription
labelstringItem label
iconOutlinedElementType | stringDefault icon
iconFilledElementType | stringActive/hover icon
isActivebooleanfalseActive state
onClick() => voidClick handler
childrenReactNodeNested SidebarSubItems

SidebarSubItem

PropTypeDefaultDescription
labelstringSub-item label
isActivebooleanfalseActive state
onClick() => voidClick handler

Accessibility

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

  • Primary rail and secondary panel each have their own <nav> landmark
  • All interactive items are keyboard accessible (Tab, Enter, Space)
  • Expandable items communicate state via aria-expanded
  • Active items use aria-current="page" or visual indicators
  • Collapse/expand toggle has aria-label ("Collapse sidebar" / "Expand sidebar")
  • Mobile Sheet includes proper focus trapping and Escape-to-close
  • Animations respect prefers-reduced-motion