Dashboard Layout
Application shell with sidebar/navbar variants, scroll restoration, and auth hydration.
What Problem This Solves
Every dashboard needs an application shell: header, navigation, content area, footer. This pattern provides two variants (sidebar and navbar) with auth state hydration and scroll restoration.
Prerequisites
- React
- Zustand (for layout state)
- Your UI library (Ant Design, shadcn/ui, etc.)
Files
use-page-layout.ts (Zustand Store)
ts
import { create } from "zustand";
type PageLayoutState = {
pageTitle: string;
breadCrumbItems: Array<{ title: string; href?: string }>;
setPageMeta: (meta: {
pageTitle?: string;
breadCrumbItems?: PageLayoutState["breadCrumbItems"];
}) => void;
};
export const usePageLayout = create<PageLayoutState>((set) => ({
pageTitle: "",
breadCrumbItems: [],
setPageMeta: (meta) => set((state) => ({ ...state, ...meta })),
}));sidebar-layout.tsx
tsx
import { Layout, Menu } from "antd";
import { Outlet, useLocation } from "react-router";
import { usePageLayout } from "./use-page-layout";
const { Sider, Content, Header } = Layout;
export function SidebarLayout() {
const location = useLocation();
const { pageTitle, breadCrumbItems } = usePageLayout();
return (
<Layout style={{ minHeight: "100vh" }}>
<Sider breakpoint="lg" collapsedWidth="80">
<div className="logo">MyApp</div>
<Menu
theme="dark"
mode="inline"
selectedKeys={[location.pathname]}
items={[
{ key: "/dashboard", label: "Dashboard" },
{ key: "/users", label: "Users" },
{ key: "/settings", label: "Settings" },
]}
/>
</Sider>
<Layout>
<Header style={{ background: "#fff", padding: "0 24px" }}>
<h2>{pageTitle}</h2>
</Header>
<Content style={{ margin: "24px 16px", padding: 24, background: "#fff" }}>
<Outlet />
</Content>
</Layout>
</Layout>
);
}navbar-layout.tsx
tsx
import { Menu } from "antd";
import { Outlet, useLocation } from "react-router";
import { usePageLayout } from "./use-page-layout";
export function NavbarLayout() {
const location = useLocation();
const { pageTitle } = usePageLayout();
return (
<div className="min-h-screen flex flex-col">
<header className="border-b bg-white px-6 py-4">
<div className="flex items-center justify-between">
<h1 className="text-xl font-bold">MyApp</h1>
<Menu
mode="horizontal"
selectedKeys={[location.pathname]}
items={[
{ key: "/dashboard", label: "Dashboard" },
{ key: "/users", label: "Users" },
{ key: "/settings", label: "Settings" },
]}
/>
</div>
</header>
<main className="flex-1 p-6">
<h2 className="mb-4 text-2xl font-semibold">{pageTitle}</h2>
<Outlet />
</main>
</div>
);
}page-layout.tsx (Declarative Meta)
tsx
import { useEffect } from "react";
import { usePageLayout } from "./use-page-layout";
type PageLayoutProps = {
children: React.ReactNode;
header: string;
breadCrumbItems?: Array<{ title: string; href?: string }>;
};
export function PageLayout({ children, header, breadCrumbItems }: PageLayoutProps) {
const setPageMeta = usePageLayout((s) => s.setPageMeta);
useEffect(() => {
setPageMeta({ pageTitle: header, breadCrumbItems });
}, [header, breadCrumbItems, setPageMeta]);
return <div>{children}</div>;
}Usage
tsx
// In your route component
import { PageLayout } from "./page-layout";
export function DashboardPage() {
return (
<PageLayout header="Dashboard" breadCrumbItems={[{ title: "Home" }, { title: "Dashboard" }]}>
<DashboardContent />
</PageLayout>
);
}Variants
| Variant | Best For | Mobile Behavior |
|---|---|---|
| Sidebar | Data-heavy admin panels, many nav items | Collapses to icon-only, hamburger on small screens |
| Navbar | Content-focused apps, fewer nav items | Horizontal scroll or hamburger menu |