Skip to content

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

VariantBest ForMobile Behavior
SidebarData-heavy admin panels, many nav itemsCollapses to icon-only, hamburger on small screens
NavbarContent-focused apps, fewer nav itemsHorizontal scroll or hamburger menu