SidebarLayout
SidebarLayout is the default layout component that provides a responsive sidebar navigation, breadcrumb trail, and theme toggle. It's designed to work seamlessly with AppShell's module system.
Import
import { SidebarLayout } from "@tailor-platform/app-shell";Basic Usage
import { AppShell, SidebarLayout } from "@tailor-platform/app-shell";
function App() {
return (
<AppShell modules={modules}>
<SidebarLayout />
</AppShell>
);
}This gives you:
- ✅ Responsive sidebar with auto-generated navigation from modules
- ✅ Breadcrumb navigation
- ✅ Theme toggle (light/dark mode)
- ✅ Mobile-friendly collapsible sidebar
Props
children
- Type:
(props: { Outlet: () => React.ReactNode }) => React.ReactNode(optional) - Description: Custom content renderer for adding headers, footers, or wrapping the outlet
<SidebarLayout>
{({ Outlet }) => (
<>
<CustomHeader />
<Outlet />
<CustomFooter />
</>
)}
</SidebarLayout>The Outlet component renders your current route's component.
sidebar
- Type:
React.ReactNode(optional) - Default:
<SidebarLayout.DefaultSidebar /> - Description: Replaces the whole sidebar region. Omit it for the built-in sidebar.
import { SidebarLayout, SidebarItem } from "@tailor-platform/app-shell";
<SidebarLayout
sidebar={
<SidebarLayout.DefaultSidebar>
<SidebarItem label="Custom Link" href="/custom" />
</SidebarLayout.DefaultSidebar>
}
/>;
SidebarLayout.DefaultSidebaris the same component as the top-levelDefaultSidebarexport (kept for backwards compatibility). The namespaced form is preferred for discoverability — it pairs withSidebarLayout.DefaultHeader.
defaultOpen
- Type:
boolean(optional) - Default:
true - Description: Whether the sidebar is open by default on desktop. Has no effect when
collapsibleisfalse.
// Sidebar closed by default on desktop
<SidebarLayout defaultOpen={false} />collapsible
- Type:
boolean(optional) - Default:
true - Description: Whether the sidebar can be collapsed. When set to
false, the sidebar is always visible and toggle buttons are hidden.defaultOpenis ignored when this isfalse.
// Non-collapsible sidebar (always visible, toggle buttons hidden)
<SidebarLayout collapsible={false} />header
- Type:
React.ReactNode(optional) - Default:
<SidebarLayout.DefaultHeader /> - Description: Replaces the whole top-bar region. Omit it for the built-in header.
Like sidebar, header is a full-region slot. There are three levels of customization:
1. Default — omit header entirely:
<SidebarLayout />2. Extend the built-in header — pass SidebarLayout.DefaultHeader and use its actions slot. This is the common case (e.g. adding a notification bell) and keeps the trigger + breadcrumb without reconstructing them:
import { SidebarLayout, AppearanceSwitcher, Button } from "@tailor-platform/app-shell";
import { BellIcon } from "lucide-react";
<SidebarLayout
header={
<SidebarLayout.DefaultHeader
actions={[
<Button key="bell" variant="outline" size="icon" aria-label="Notifications">
<BellIcon />
</Button>,
// `actions` REPLACES the default right-hand cluster, so include the
// appearance switcher explicitly to keep it.
<AppearanceSwitcher key="appearance" />,
]}
/>
}
/>;3. Replace it entirely — supply your own node:
<SidebarLayout header={<MyCustomHeader />} />SidebarLayout.DefaultHeader
The built-in header: sidebar trigger + breadcrumb on the left, and the actions cluster on the right.
actions—React.ReactNode | React.ReactNode[](optional). The entire right-hand cluster, laid out in a horizontal, vertically-centered row with consistent spacing.- Default:
[<AppearanceSwitcher />]— so out-of-the-box behavior is unchanged. - ⚠️
actionsreplaces the whole right-hand cluster, including the appearance switcher. If you pass your own actions and still want the switcher, include<AppearanceSwitcher />in the array (it is a public export).actions={[]}renders an empty right side.
- Default:
This is the supported extension point for the top bar — it replaces fragile workarounds that queried the header DOM and injected a React portal.
Features
Responsive Sidebar
The sidebar automatically adapts to different screen sizes:
- Desktop (≥ 768px): Full sidebar visible by default
- Tablet/Mobile (< 768px): Collapsible sidebar with hamburger menu
Users can toggle the sidebar using:
- Hamburger menu button (top left)
- Keyboard shortcut:
Cmd + B/Ctrl + B
Breadcrumb Navigation
Breadcrumbs are automatically generated based on the current route:
Dashboard > Products > Product DetailsBreadcrumbs update automatically as users navigate through your application.
Theme Toggle
The built-in header renders an AppearanceSwitcher — a palette-icon button whose dropdown switches the color theme (persisted to localStorage). To add your own controls (notifications, user menu, etc.) alongside it, pass SidebarLayout.DefaultHeader with an actions array that includes <AppearanceSwitcher />.
Customization Examples
Custom Header and Footer
import { SidebarLayout } from "@tailor-platform/app-shell";
const CustomHeader = () => (
<div className="astw:bg-blue-500 astw:text-white astw:p-4">
<h2>Welcome to My App</h2>
</div>
);
const CustomFooter = () => (
<footer className="astw:p-4 astw:text-sm astw:text-gray-600">© 2026 My Company</footer>
);
function App() {
return (
<AppShell modules={modules}>
<SidebarLayout>
{({ Outlet }) => (
<div className="astw:flex astw:flex-col astw:h-full">
<CustomHeader />
<main className="astw:flex-1 astw:overflow-auto">
<Outlet />
</main>
<CustomFooter />
</div>
)}
</SidebarLayout>
</AppShell>
);
}Custom Sidebar
import {
SidebarLayout,
DefaultSidebar,
SidebarItem,
SidebarGroup,
SidebarSeparator,
} from "@tailor-platform/app-shell";
import { HelpCircle, ExternalLink } from "lucide-react";
function App() {
return (
<AppShell modules={modules}>
<SidebarLayout
sidebar={
<DefaultSidebar>
{/* Auto-generated navigation from modules */}
{/* Add custom items */}
<SidebarSeparator />
<SidebarGroup label="Help">
<SidebarItem
label="Documentation"
icon={<HelpCircle />}
href="https://docs.example.com"
external
/>
<SidebarItem
label="Support"
icon={<ExternalLink />}
href="https://support.example.com"
external
/>
</SidebarGroup>
</DefaultSidebar>
}
/>
</AppShell>
);
}Wrapping Content in a Container
<SidebarLayout>
{({ Outlet }) => (
<div className="astw:container astw:mx-auto astw:p-6 astw:max-w-7xl">
<Outlet />
</div>
)}
</SidebarLayout>Adding a Global Banner
<SidebarLayout>
{({ Outlet }) => (
<>
<div className="astw:bg-yellow-100 astw:border-b astw:border-yellow-200 astw:p-3 astw:text-center">
<p className="astw:text-sm">🎉 New features available! Check out our latest updates.</p>
</div>
<Outlet />
</>
)}
</SidebarLayout>Layout Structure
The SidebarLayout component creates the following structure:
┌─────────────────────────────────────────────┐
│ Sidebar │ Header (Breadcrumbs + │
│ │ Theme Toggle) │
│ - Dashboard ├─────────────────────────┤
│ - Products │ │
│ - Orders │ │
│ │ Page Content │
│ [Settings ▼] │ (Outlet) │
│ │ │
│ │ │
└─────────────────────────────────────────────┘Mobile view (sidebar collapsed):
┌─────────────────────────────────┐
│ [☰] Breadcrumbs [Theme] │
├─────────────────────────────────┤
│ │
│ Page Content │
│ (Outlet) │
│ │
└─────────────────────────────────┘Accessing the content scroll container
The AppShell is viewport-bounded (h-svh), so the document itself does not scroll — the content area does. Code that previously relied on window/document scroll (reading window.scrollY, listening to window's scroll event, window.scrollTo(...), or an IntersectionObserver with the default viewport root) should target the content scroll container instead.
Use the useAppShellScrollContainer() hook to get a ref to that element from any page:
import { useAppShellScrollContainer } from "@tailor-platform/app-shell";
import { useEffect, useState } from "react";
function ReadingProgress() {
const scrollRef = useAppShellScrollContainer();
const [progress, setProgress] = useState(0);
useEffect(() => {
const el = scrollRef.current;
if (!el) return;
const onScroll = () => {
const max = el.scrollHeight - el.clientHeight;
setProgress(max > 0 ? el.scrollTop / max : 0);
};
onScroll();
el.addEventListener("scroll", onScroll, { passive: true });
return () => el.removeEventListener("scroll", onScroll);
}, [scrollRef]);
return <progress value={progress} />;
}The same element handles imperative scrolling (scrollRef.current?.scrollTo({ top: 0, behavior: "smooth" })) and works as an IntersectionObserver root (new IntersectionObserver(cb, { root: scrollRef.current })).
Notes:
- The element mounts with the layout, above your page, so read
ref.currentinside an effect — it is populated by the time effects run, not during render. - On a
<Layout fill>page this element does not scroll; its children (e.g. aDataTable) manage their own scrolling. - Outside a
SidebarLayout(a fully custom layout) the returned ref'scurrentis alwaysnull— such layouts own their own scroll region. - For non-React access (CSS, tests, plain DOM) the container also carries a
data-appshell-scroll-containerattribute:document.querySelector("[data-appshell-scroll-container]").
Styling
The sidebar and layout use Tailwind CSS classes prefixed with astw: to avoid conflicts with your application styles.
To customize the appearance, you can:
Override CSS variables in your theme.css:
css:root { --sidebar-width: 280px; /* Default: 256px */ }Use custom sidebar component with your own styling
Wrap Outlet with container classes as shown in examples above
Accessibility
SidebarLayout includes built-in accessibility features:
- Keyboard navigation: Navigate sidebar items with arrow keys
- ARIA labels: Proper labels for screen readers
- Focus management: Focus trap when sidebar is open on mobile
- Responsive: Works with keyboard and touch inputs
Related Components
- AppShell - Root component
- DefaultHeader - Built-in header (
SidebarLayout.DefaultHeader) - DefaultSidebar - Built-in sidebar (
SidebarLayout.DefaultSidebar) - AppearanceSwitcher - Color-theme dropdown, composable into header
actions - SidebarItem - Individual sidebar navigation items
- SidebarGroup - Group sidebar items
Related Concepts
- Modules and Resources - How navigation is generated
- Routing and Navigation - Navigation between pages
- Styling and Theming - Customize appearance
API Reference
- useTheme - Access theme context