SidebarLayout
SidebarLayout is the default AppShell layout. By default it renders:
SidebarLayout.DefaultSidebarSidebarLayout.DefaultHeader- the current route outlet
You can replace the sidebar, replace the content header, add a full-width topBar above the whole shell, or take over the entire region beside the sidebar with the body slot.
If you want the opinionated "global app header + icon rail" mode, use GlobalHeaderLayout, which is built on top of SidebarLayout.
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 via the built-in header
- ✅ Mobile-friendly collapse behavior
Props
Common props
| Prop | Type | Default | Description |
|---|---|---|---|
sidebar | React.ReactNode | <SidebarLayout.DefaultSidebar /> | Replaces the entire sidebar region. |
defaultOpen | boolean | true | Whether the sidebar starts expanded on desktop. |
collapsible | boolean | true | Whether the sidebar can be collapsed. When false, toggle controls are hidden and defaultOpen is ignored. |
topBar | React.ReactNode | - | Full-width bar above both the sidebar and the content region. |
Default content mode
Use this when AppShell should keep owning the content column.
| Prop | Type | Default | Description |
|---|---|---|---|
header | React.ReactNode | <SidebarLayout.DefaultHeader /> | Replaces the content header above the main column. |
children | (props: { Outlet: () => React.ReactNode }) => React.ReactNode | current route outlet | Custom content renderer that wraps or surrounds the outlet. |
Body-slot mode
Use this when you need to compose your own columns beside the sidebar.
| Prop | Type | Default | Description |
|---|---|---|---|
body | React.ReactNode | required | Replaces everything to the right of the sidebar. Compose it with ContentContainer, Outlet, and Trigger. |
body is mutually exclusive with header and children, because it replaces the region those props normally describe.
Extending the built-in header
The supported "small customization" path is to pass SidebarLayout.DefaultHeader into the header slot and fill its actions cluster.
import { AppearanceSwitcher, Button, SidebarLayout } from "@tailor-platform/app-shell";
import { BellIcon } from "lucide-react";
<SidebarLayout
header={
<SidebarLayout.DefaultHeader
actions={[
<Button key="notifications" variant="outline" size="icon" aria-label="Notifications">
<BellIcon className="size-4" />
</Button>,
<AppearanceSwitcher key="appearance" />,
]}
/>
}
/>;actions replaces the entire right-hand cluster. If you still want the built-in appearance switcher, include <AppearanceSwitcher /> yourself.
Replacing the sidebar
Pass sidebar to replace the whole left-side region. The built-in component is SidebarLayout.DefaultSidebar, also exported as DefaultSidebar.
import {
SidebarGroup,
SidebarItem,
SidebarLayout,
SidebarSeparator,
} from "@tailor-platform/app-shell";
<SidebarLayout
sidebar={
<SidebarLayout.DefaultSidebar>
<SidebarItem to="/" />
<SidebarSeparator />
<SidebarGroup title="Main">
<SidebarItem to="/dashboard" />
<SidebarItem to="/orders" />
</SidebarGroup>
</SidebarLayout.DefaultSidebar>
}
/>;Adding an app-wide top bar
topBar renders above the whole shell, spanning both the sidebar and the content region.
<SidebarLayout topBar={<MyGlobalTopBar />} sidebar={<SidebarLayout.DefaultSidebar hideHeader />} />When topBar is present, the fixed sidebar is offset below it. The bar should be 3.5rem tall (h-14) to match the built-in layout.
If you specifically want AppShell's opinionated global-header mode, use GlobalHeaderLayout, which wires the top bar, icon rail, and matching default sidebar for you.
Owning the region beside the sidebar
Use body when the stock content column is too restrictive and you need your own side columns.
<SidebarLayout
body={
<>
<aside className="w-64 shrink-0 overflow-y-auto border-r">
<TableOfContents />
</aside>
<SidebarLayout.ContentContainer header={<SidebarLayout.DefaultHeader />}>
<SidebarLayout.Outlet />
</SidebarLayout.ContentContainer>
<aside className="w-96 shrink-0 overflow-y-auto border-l">
<AssistantPanel />
</aside>
</>
}
/>SidebarLayout.ContentContainer is the stock main column: inset padding, pinned header slot, scroll region, and useAppShellScrollContainer() support. SidebarLayout.Trigger exposes the built-in sidebar toggle so custom headers do not need DOM workarounds.
Reading and controlling the sidebar
Use useAppShellSidebar() to read and control the sidebar state instead of observing internal DOM attributes or clicking the trigger through the DOM.
import { useAppShellSidebar } from "@tailor-platform/app-shell";
function CustomHeader() {
const { open, isMobile, toggle } = useAppShellSidebar();
return (
<div className="flex items-center justify-between border-b px-4 py-3">
<button type="button" onClick={toggle}>
{open ? "Hide navigation" : "Show navigation"}
</button>
<span>{isMobile ? "Mobile" : "Desktop"}</span>
</div>
);
}Outside SidebarLayout, the hook returns a safe fallback (open: true, isMobile: false, no-op setters) rather than throwing.
Reusing the built-in breadcrumb
DynamicBreadcrumb is exported when you want the same route-driven breadcrumb in a custom header or top bar:
import { DynamicBreadcrumb, SidebarLayout } from "@tailor-platform/app-shell";
<header className="flex items-center gap-3 border-b px-4 py-3">
<SidebarLayout.Trigger />
<DynamicBreadcrumb />
</header>;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 (for example 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 are styled with the library's internal astw:-prefixed utilities. Write plain Tailwind utilities in your own markup — see Styling AppShell components.
Accessibility
SidebarLayout includes built-in accessibility features:
- Keyboard navigation for sidebar interaction
- ARIA labels on built-in controls
- Focus management when the sidebar opens on mobile
- Responsive behavior that works with keyboard, mouse, and touch input
Related
- AppShell - root component
- DefaultHeader - built-in content header (
SidebarLayout.DefaultHeader) - DefaultSidebar - built-in sidebar (
SidebarLayout.DefaultSidebar) - GlobalHeaderLayout - opinionated app-wide header mode built on SidebarLayout
- AppearanceSwitcher - color-theme dropdown for header actions
- usePageMeta - route metadata lookup used by sidebar items