Sidebar + Topbar App Shell
Application shell with a flush sidebar and a full application topbar carrying breadcrumbs, command search, notifications, and the account menu.
pnpm dlx shadcn@latest add @cosskit/app-shell-topbarRequires the @cosskit namespace in your components.json.
Code
import {
BellIcon,
BookOpenIcon,
BuildingIcon,
ChevronsUpDownIcon,
CreditCardIcon,
FileTextIcon,
LayoutDashboardIcon,
PlugIcon,
PlusIcon,
SearchIcon,
SettingsIcon,
SparklesIcon,
UsersIcon,
WorkflowIcon,
} from "lucide-react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Button } from "@/components/ui/button";
import { Kbd, KbdGroup } from "@/components/ui/kbd";
import {
Menu,
MenuItem,
MenuPopup,
MenuSeparator,
MenuTrigger,
} from "@/components/ui/menu";
import { Separator } from "@/components/ui/separator";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarRail,
SidebarTrigger,
} from "@/components/ui/sidebar";
// ---------------------------------------------------------------------------
// Navigation
// ---------------------------------------------------------------------------
const mainNav = [
{ icon: LayoutDashboardIcon, label: "Overview", href: "#" },
{ icon: UsersIcon, label: "Customers", href: "#", isActive: true, badge: "248" },
{ icon: FileTextIcon, label: "Invoices", href: "#", badge: "6" },
{ icon: WorkflowIcon, label: "Automations", href: "#" },
{ icon: BookOpenIcon, label: "Reports", href: "#" },
];
const workspaceNav = [
{ icon: UsersIcon, label: "Team", href: "#" },
{ icon: PlugIcon, label: "Integrations", href: "#" },
{ icon: SettingsIcon, label: "Settings", href: "#" },
];
// ---------------------------------------------------------------------------
// Page
//
// Differs from `app-shell-inset` in three ways worth knowing before you pick
// one: the sidebar is flush (`variant="sidebar"`) rather than an inset panel,
// it collapses off-canvas rather than to an icon rail, and the account menu
// lives in the topbar instead of the sidebar footer — which frees the footer
// for a plan/upgrade slot.
// ---------------------------------------------------------------------------
export default function AppShellTopbar() {
return (
<SidebarProvider className="[&_[data-slot=button]]:transition-[box-shadow,scale] [&_[data-slot=button]:not(.w-full)]:motion-safe:hover:scale-[1.05] [&_[data-slot=button].w-full]:motion-safe:hover:scale-[1.03]">
<Sidebar variant="sidebar" collapsible="offcanvas">
{/* Brand / workspace switcher */}
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<Menu>
<MenuTrigger
render={
<SidebarMenuButton
size="lg"
className="cursor-pointer data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
/>
}
>
<div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-primary font-bold text-primary-foreground text-sm">
K
</div>
<div className="flex min-w-0 flex-col leading-tight">
<span className="truncate font-semibold text-sm">Kestrel</span>
<span className="truncate text-muted-foreground text-xs">
Growth plan
</span>
</div>
<ChevronsUpDownIcon className="ms-auto size-4 shrink-0" />
</MenuTrigger>
{/* Pointer cursor is a house rule; coss ships `cursor-default`
here by native-menu convention. It goes on the popup — which
portals out of this block, so no ancestor rule can reach it —
and never by editing coss source. The `-item` suffix covers
checkbox and radio items too, so later additions inherit it. */}
<MenuPopup
align="start"
className="min-w-56 [&_[data-slot$='-item']]:cursor-pointer"
sideOffset={8}
>
<MenuItem>
<BuildingIcon />
Kestrel
</MenuItem>
<MenuItem>
<BuildingIcon />
Harborview Media
</MenuItem>
<MenuSeparator />
<MenuItem>
<PlusIcon />
New workspace
</MenuItem>
</MenuPopup>
</Menu>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Main</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{mainNav.map(({ icon: Icon, label, href, isActive, badge }) => (
<SidebarMenuItem key={label}>
<SidebarMenuButton
isActive={isActive}
render={<a href={href} />}
tooltip={label}
>
<Icon />
<span>{label}</span>
</SidebarMenuButton>
{badge && <SidebarMenuBadge>{badge}</SidebarMenuBadge>}
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Workspace</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{workspaceNav.map(({ icon: Icon, label, href }) => (
<SidebarMenuItem key={label}>
<SidebarMenuButton render={<a href={href} />} tooltip={label}>
<Icon />
<span>{label}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
{/* Footer is free for a plan slot because the account menu is in the
topbar. `bg-sidebar-accent` keeps it on the sidebar's own surface
ramp rather than the page canvas. */}
<SidebarFooter>
<div className="flex flex-col gap-2 rounded-lg bg-sidebar-accent p-3">
<div className="flex items-center gap-2">
<SparklesIcon className="size-4 shrink-0" />
<span className="font-medium text-sm">8 of 10 seats used</span>
</div>
<p className="text-muted-foreground text-xs">
Upgrade to Scale for unlimited seats, audit logs, and SSO.
</p>
<Button className="w-full" size="sm">
Upgrade plan
</Button>
</div>
</SidebarFooter>
<SidebarRail />
</Sidebar>
<SidebarInset>
{/* Application topbar */}
<header className="sticky top-0 z-10 flex h-14 shrink-0 items-center gap-2 border-b bg-background px-4">
<SidebarTrigger className="-ms-1" />
<Separator className="h-5 max-sm:hidden" orientation="vertical" />
<Breadcrumb className="max-sm:hidden">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink render={<a href="#" />}>Kestrel</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink render={<a href="#" />}>Customers</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Acme Industries</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<span className="font-medium text-sm sm:hidden">Acme Industries</span>
<div className="ms-auto flex items-center gap-2">
{/* Command-palette affordance. Inert here — wire it to your own
palette (coss ships `Command`) in the consuming app. */}
<Button
className="w-56 justify-start gap-2 text-muted-foreground max-lg:hidden"
size="sm"
variant="outline"
>
<SearchIcon />
<span className="flex-1 text-left">Search customers…</span>
<KbdGroup>
<Kbd>⌘</Kbd>
<Kbd>K</Kbd>
</KbdGroup>
</Button>
<Button
aria-label="Search"
className="lg:hidden"
size="icon-sm"
variant="ghost"
>
<SearchIcon />
</Button>
<Button
aria-label="Notifications — 3 unread"
className="relative"
size="icon-sm"
variant="ghost"
>
<BellIcon />
<span className="absolute end-1.5 top-1.5 size-1.5 rounded-full bg-primary" />
</Button>
<Separator className="h-5 max-sm:hidden" orientation="vertical" />
<Menu>
<MenuTrigger
render={
<Button
aria-label="Account menu"
className="rounded-full"
size="icon-sm"
variant="ghost"
/>
}
>
<Avatar className="size-7 rounded-full">
<AvatarFallback className="text-xs">NR</AvatarFallback>
</Avatar>
</MenuTrigger>
<MenuPopup
align="end"
className="min-w-56 [&_[data-slot$='-item']]:cursor-pointer"
sideOffset={8}
>
<div className="flex flex-col gap-0.5 px-2 py-1.5">
<p className="font-medium text-sm leading-none">Nadia Rahim</p>
<p className="truncate text-muted-foreground text-xs">
nadia@kestrel.io
</p>
</div>
<MenuSeparator />
<MenuItem>
<SettingsIcon />
Account settings
</MenuItem>
<MenuItem>
<CreditCardIcon />
Billing
</MenuItem>
<MenuItem>
<UsersIcon />
Invite teammates
</MenuItem>
<MenuSeparator />
<MenuItem variant="destructive">Sign out</MenuItem>
</MenuPopup>
</Menu>
</div>
</header>
{/* Page content */}
<div className="flex flex-1 flex-col gap-6 p-4 sm:p-6">
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="flex flex-col gap-1">
<h1 className="font-semibold text-2xl tracking-tight">
Acme Industries
</h1>
<p className="text-muted-foreground text-sm">
Enterprise account · 42 seats · customer since March 2024
</p>
</div>
<Button size="sm">
<PlusIcon />
New invoice
</Button>
</div>
<div className="flex min-h-80 flex-1 items-center justify-center rounded-xl border border-dashed">
<div className="flex flex-col items-center gap-2 text-center">
<LayoutDashboardIcon className="size-8 text-muted-foreground/50" />
<p className="font-medium text-muted-foreground text-sm">
Your page content goes here
</p>
<p className="max-w-xs text-muted-foreground/70 text-xs">
Compose this area from other blocks — a data table, KPI cards, or
a settings form.
</p>
</div>
</div>
</div>
</SidebarInset>
</SidebarProvider>
);
}
Dependencies
- @coss/ui