App Sidebar
App shell with a sticky left sidebar: workspace header, grouped nav with yellow active state and counters, usage meter, user footer. Becomes a top bar + slide-over drawer below lg.
brutalist/layout/app-sidebarSource
"use client";
import { useEffect, useId, useState, type ReactNode } from "react";
import {
BarChart3,
Bell,
BookOpen,
FileText,
LayoutGrid,
Menu,
Plug,
Settings,
Users,
X,
type LucideIcon,
} from "lucide-react";
import { cn } from "@/lib/utils";
export interface AppSidebarItem {
label: string;
href: string;
icon: LucideIcon;
/** Small counter on the right (e.g. unread items). */
count?: number;
}
export interface AppSidebarSection {
title?: string;
items: AppSidebarItem[];
}
export interface AppSidebarProps {
workspace?: { name: string; plan: string };
sections?: AppSidebarSection[];
/** href of the current page (gets aria-current="page"). */
activeHref?: string;
user?: { name: string; email: string };
/** Promo/usage block at the bottom of the sidebar. */
footer?: ReactNode;
/** Page content rendered next to the sidebar. */
children?: ReactNode;
className?: string;
}
const DEFAULT_SECTIONS: AppSidebarSection[] = [
{
items: [
{ label: "Overview", href: "#overview", icon: LayoutGrid },
{ label: "Releases", href: "#releases", icon: FileText, count: 3 },
{ label: "Announcements", href: "#announcements", icon: Bell },
{ label: "Analytics", href: "#analytics", icon: BarChart3 },
],
},
{
title: "Workspace",
items: [
{ label: "Members", href: "#members", icon: Users },
{ label: "Integrations", href: "#integrations", icon: Plug },
{ label: "Settings", href: "#settings", icon: Settings },
],
},
{
title: "Help",
items: [{ label: "Documentation", href: "#docs", icon: BookOpen }],
},
];
function initials(name: string): string {
return name
.split(" ")
.map((p) => p[0])
.join("")
.slice(0, 2)
.toUpperCase();
}
export function AppSidebar({
workspace = { name: "Acme Inc.", plan: "Team plan" },
sections = DEFAULT_SECTIONS,
activeHref = "#releases",
user = { name: "Maya Okafor", email: "maya@acme.dev" },
footer,
children,
className,
}: AppSidebarProps) {
const [open, setOpen] = useState(false);
const drawerId = useId();
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => e.key === "Escape" && setOpen(false);
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open]);
const nav = (
<nav aria-label="Sidebar" className="flex flex-1 flex-col gap-6 overflow-y-auto px-3 py-5">
{sections.map((section, i) => (
<div key={section.title ?? i}>
{section.title && (
<p className="px-3 pb-2 font-da-mono text-[11px] font-bold tracking-da-label text-da-muted-fg uppercase">{section.title}</p>
)}
<ul className="flex flex-col gap-1">
{section.items.map((item) => {
const Icon = item.icon;
const active = item.href === activeHref;
return (
<li key={item.href}>
<a
href={item.href}
aria-current={active ? "page" : undefined}
onClick={() => setOpen(false)}
className={cn(
"da-focus da-transition flex items-center gap-3 border-[length:var(--da-border-width)] px-3 py-2 font-bold",
active
? "border-da-border bg-da-primary text-da-primary-fg shadow-da-sm"
: "border-transparent hover:border-da-border hover:bg-da-surface",
)}
>
<Icon aria-hidden className="size-5 shrink-0" strokeWidth={2.25} />
<span className="flex-1 truncate">{item.label}</span>
{item.count !== undefined && (
<span className="da-stroke bg-da-fg px-1.5 font-da-mono text-[11px] text-da-bg">
{item.count}
<span className="sr-only"> new</span>
</span>
)}
</a>
</li>
);
})}
</ul>
</div>
))}
</nav>
);
const panel = (
<>
<div className="da-stroke-b flex items-center gap-3 px-4 py-4">
<span aria-hidden className="da-stroke grid size-10 shrink-0 place-items-center bg-da-accent font-da-display text-lg text-da-accent-fg">
{workspace.name[0]}
</span>
<div className="min-w-0">
<p className="truncate font-bold">{workspace.name}</p>
<p className="font-da-mono text-[11px] tracking-da-label text-da-muted-fg uppercase">{workspace.plan}</p>
</div>
</div>
{nav}
{footer ?? (
<div className="px-3 pb-3">
<div className="da-stroke bg-da-secondary p-4 text-da-secondary-fg">
<p className="font-da-mono text-[11px] font-bold tracking-da-label uppercase">Usage · September</p>
<p className="mt-1 font-bold">7 of 10 projects</p>
<div className="da-stroke mt-3 h-3 bg-da-surface" role="progressbar" aria-valuenow={70} aria-valuemin={0} aria-valuemax={100} aria-label="Projects used">
<div className="h-full w-[70%] bg-da-primary" />
</div>
</div>
</div>
)}
<div className="da-stroke-t flex items-center gap-3 px-4 py-3">
<span aria-hidden className="da-stroke grid size-9 shrink-0 place-items-center rounded-full bg-da-surface font-da-mono text-xs font-bold text-da-surface-fg">
{initials(user.name)}
</span>
<div className="min-w-0 text-sm">
<p className="truncate font-bold">{user.name}</p>
<p className="truncate text-da-muted-fg">{user.email}</p>
</div>
</div>
</>
);
return (
<div className={cn("flex min-h-full bg-da-bg text-da-fg", className)}>
{/* Desktop */}
<aside className="da-stroke-r sticky top-0 hidden h-dvh w-64 shrink-0 flex-col bg-da-surface-2 lg:flex">{panel}</aside>
{/* Mobile drawer */}
{open && (
<div className="fixed inset-0 z-50 lg:hidden">
<button type="button" aria-label="Close menu" onClick={() => setOpen(false)} className="absolute inset-0 bg-da-overlay" />
<aside id={drawerId} className="da-stroke-r relative flex h-full w-72 max-w-[85vw] flex-col bg-da-surface-2 shadow-da-lg">
<button
type="button"
aria-label="Close menu"
onClick={() => setOpen(false)}
className="da-focus da-stroke absolute top-4 -right-14 grid size-10 place-items-center bg-da-surface text-da-surface-fg"
>
<X aria-hidden className="size-5" />
</button>
{panel}
</aside>
</div>
)}
<div className="flex min-w-0 flex-1 flex-col">
<div className="da-stroke-b flex items-center gap-3 bg-da-surface-2 px-4 py-3 lg:hidden">
<button
type="button"
aria-label="Open menu"
aria-expanded={open}
aria-controls={drawerId}
onClick={() => setOpen(true)}
className="da-focus da-stroke grid size-10 place-items-center bg-da-surface text-da-surface-fg shadow-da-sm"
>
<Menu aria-hidden className="size-5" />
</button>
<p className="font-bold">{workspace.name}</p>
</div>
<main className="flex-1">{children}</main>
</div>
</div>
);
}
export default AppSidebar;
modules/brutalist/layout/app-sidebar/index.tsx
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | Page content, rendered in <main> next to the sidebar. |
| workspace | { name: string; plan: string } | Acme Inc. / Team plan | Workspace block at the top. |
| sections | AppSidebarSection[] | — | { title?, items: { label, href, icon: LucideIcon, count? }[] }[]. |
| activeHref | string | "#releases" | href of the current page (aria-current=page). With Next.js pass usePathname(). |
| user | { name: string; email: string } | — | User block at the bottom. |
| footer | ReactNode | — | Replaces the default usage meter. |
| className | string | — | Classes on the root. |
Other sidebar variants in Brutalist
Icon Rail
Compact 80px icon rail: ink brand square, square icon links with yellow active state, red counters and ink tooltips on hover/focus, settings pinned at the bottom. Server-safe, CSS-only tooltips.
Sidebar Collapsible
App sidebar that collapses from 256px to a 76px icon column with a stepped width animation; ink active item, labels become sr-only when collapsed, collapse toggle pinned at the bottom.
Sidebar Dual
Two-level navigation: an ink icon rail switches sections, and a paper second panel lists that section's links with counts and a yellow active link. Second panel hides on very small screens.
Sidebar in other art directions
App Sidebar
Floating frosted sidebar inset from the viewport edges: gradient-orb brand, pill nav items with an ink active state and gradient badges, workspace group, gradient promo card and user chip. Becomes a floating pill top bar + glass drawer below lg.
Sidebar Adaptive
Adaptive navigation: a full glass sidebar with brand, gradient action button, labelled links and a usage meter on desktop; a floating bottom tab bar with a raised gradient “+” on mobile.
Sidebar Channels
Chat-app style double sidebar: a column of gradient workspace squircles plus a glass channel panel with collapsible sections, private/live icons, unread counts and LIVE meeting badges; drawer on mobile.
Sidebar Floating
Floating glass icon rail detached from the viewport edges: gradient logo orb, gradient active item, unread badges, glass tooltips on hover/focus, settings and avatar at the bottom; a floating bottom bar on mobile.
App Sidebar
Linear-style app shell: 240px translucent sidebar with workspace switcher, search, "New" button with shortcut, grouped 32px nav rows with counters, team list with color squares and settings at the bottom. Slide-in drawer below lg.
Sidebar Inset
Inset app shell: the sidebar sits on the tinted page background while content lives in a raised rounded panel. Workspace header, collapsible nested sections with guide line, support/settings and user card; drawer on mobile.
Sidebar Rail
Sidebar that collapses between a 224px labelled list and a 56px icon rail (⌘B or bottom button) with tooltips on hover/focus and corner badges; icon-only on mobile. Controlled or uncontrolled.
Sidebar Tree
Docs / settings sidebar with a filter box and a nested collapsible tree auto-expanded to the active page (highlighted in indigo); stacks above content on mobile.
App Sidebar
Typographic app sidebar: hairline right border, logo + site switcher, mono group labels, plain text links with right-aligned mono counts; the active link is ink with a signal dot; drawer on mobile.
Sidebar Icons
Narrow icon rail (56px) of square hairline cells; the active cell is filled ink; labels appear as mono tooltips to the right on hover/focus; turns into a bottom bar on mobile.
Sidebar Ink
Ink sidebar (stays near-black in dark mode): giant numbered text links “01 Overview”, the active one full white with an arrow, a storage meter as a hairline bar at the bottom; drawer on mobile.
Sidebar Tree
CMS page tree: nested pages drawn with hairline guide lines and +/− toggles (aria-expanded), mono “Pages” header with an add button; active page ink with signal dot; drawer on mobile.
App Sidebar
Classic enterprise app shell: white bordered sidebar with brand, entity switcher, grouped nav (uppercase group labels, count badges, blue active state), settings and user footer; off-canvas drawer on mobile.
Sidebar Collapsible
Sidebar that collapses from 240px to a 64px icon rail with a toggle button (aria-expanded); collapsed items keep tooltips (title) and sr-only labels. Always a rail on small screens.
Sidebar Dual
Two-level navigation: a narrow icon rail of top-level sections (tooltips, aria-pressed) and a secondary panel listing the selected section’s pages; the panel hides below md.
Sidebar Navy
Dark navy sidebar (fg-colored in light mode, surface in dark mode — dark in both): brand, search field, nav with white-tint active state and badges, and a cash-balance card at the bottom; drawer on mobile.
App Sidebar
Calm app shell: cream sidebar with leaf logo, pill nav items (sage pill for the active page), a small “this year” footprint card with a sprout, and settings at the bottom; drawer on mobile.
Sidebar Floating
Floating sidebar: a rounded cream panel inset from the page edges with a soft shadow, italic serif group labels, a sage “New entry” pill and sage-tinted active items; drawer on mobile.
Sidebar Garden
Deep sage sidebar (dark sage tint in dark mode) with cream text, a translucent active pill, count bubbles, rolling hills illustrated along the bottom and the user’s name on the hill; drawer on mobile.
Sidebar Rail
Slim round icon rail: circular icon links on a sand column, the active one a sage circle with a leaf-shaped marker; labels appear as soft tooltips on hover/focus; becomes a bottom tab bar on mobile.
App Sidebar
Friendly app shell: warm sidebar with emoji workspace chip, big periwinkle “New task” pill, search field, nav with peach count bubbles and a colored-dot board list; slide-in drawer on mobile.
Sidebar Boards
Board navigator: starred favorites, then collapsible emoji spaces holding emoji boards with count bubbles; active board in a white pill; dashed “New space” button.
Sidebar Card
Floating white sidebar card with generous radius on a tinted page: two-dot logo, nav with lavender active pill, butter “tip of the day” card and user row; horizontal nav on mobile.
Sidebar Rail
Slim rounded icon rail: round icons whose active item fills with its own pastel color, dark pill tooltips, emoji avatar at the bottom; floating bottom bar on mobile.