Skip to content

AppShell

MD · 1950B gzip budget

Responsive app layout with sidebar, header, and main content areas.

Category
Navigation
Budget
md, several states or a live subscription
External runtime
React / React DOM only
axe fixtureSSR rendersource scanbundle budget

Preview

Live fixture
Preview locale
Header

Main content

Try the component here. Locale-aware formatting and direction follow the selected language; example content and labels are not automatically translated. Overlay demos use interactive launchers.

Install

npx shadcn@latest add https://gear5-ui.vercel.app/r/app-shell.json

Copies the source into your project. Pulls in 1 shared registry item: cn.

New here? Set up Tailwind and import aliases first →

Source & props

Edit on GitHub ↗

Prop types and inline documentation are included below. This is repository source; the installer rewrites shared imports for your project.

"use client";

import { useState } from "react";
import { cn } from "../lib/cn";

export interface AppShellProps {
  sidebar: React.ReactNode;
  header: React.ReactNode;
  children: React.ReactNode;
  className?: string;
}

/**
 * App shell layout with a collapsible sidebar, header, and main content area.
 * The sidebar toggle uses `aria-expanded` and the sidebar has
 * `aria-label="Sidebar"` for screen readers.
 */
export function AppShell({ sidebar, header, children, className }: AppShellProps) {
  const [sidebarOpen, setSidebarOpen] = useState(true);

  return (
    <div className={cn("flex h-screen overflow-hidden bg-white dark:bg-neutral-950", className)}>
      {/* Sidebar */}
      <aside
        aria-label="Sidebar"
        className={cn(
          "flex flex-col border-e border-neutral-200 bg-neutral-50 transition-[width] motion-reduce:transition-none dark:border-neutral-800 dark:bg-neutral-900",
          sidebarOpen ? "w-64" : "w-0 overflow-hidden",
        )}
      >
        <div className="flex-1 overflow-y-auto p-4">{sidebar}</div>
      </aside>

      <div className="flex flex-1 flex-col overflow-hidden">
        {/* Header */}
        <div className="flex items-center gap-2 border-b border-neutral-200 px-4 py-3 dark:border-neutral-800">
          <button
            type="button"
            aria-label={sidebarOpen ? "Collapse sidebar" : "Expand sidebar"}
            aria-expanded={sidebarOpen}
            onClick={() => setSidebarOpen((prev) => !prev)}
            className="rounded-md p-1.5 text-neutral-600 hover:bg-neutral-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:text-neutral-400 dark:hover:bg-neutral-800"
          >
            <span aria-hidden="true" className="text-lg">
              {sidebarOpen ? "◂" : "☰"}
            </span>
          </button>
          <div className="flex-1">{header}</div>
        </div>

        {/* Main content */}
        <main className="flex-1 overflow-y-auto p-6">{children}</main>
      </div>
    </div>
  );
}

What CI checks

Quality contract
  • Bundled, minified and gzipped against its tier budget
  • Audited by axe in the state previewed above
  • Rendered through react-dom/server with no browser globals
  • Scanned for network calls, dangerous sinks, and unguarded animation