Skip to content

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.

minimal/layout/app-sidebar
Open ↗

Source

"use client";

import { useEffect, useId, useState, type ReactNode } from "react";
import {
  Activity,
  BellRing,
  CalendarClock,
  ChevronsUpDown,
  FileText,
  Inbox,
  LayoutDashboard,
  PanelLeft,
  Plus,
  Radio,
  Search,
  Settings,
  type LucideIcon,
} from "lucide-react";
import { cn } from "@/lib/utils";

export interface AppSidebarItem {
  label: string;
  href: string;
  icon: LucideIcon;
  count?: number;
}

export interface AppSidebarGroup {
  label?: string;
  items: AppSidebarItem[];
}

export interface AppSidebarProps {
  workspace?: { name: string; initials?: string };
  groups?: AppSidebarGroup[];
  activeHref?: string;
  /** Teams shown as a collapsible-looking list with colored squares. */
  teams?: { name: string; href: string; color: string }[];
  primaryAction?: { label: string; onClick?: () => void };
  children?: ReactNode;
  className?: string;
}

const DEFAULT_GROUPS: AppSidebarGroup[] = [
  {
    items: [
      { label: "Inbox", href: "#inbox", icon: Inbox, count: 4 },
      { label: "Incidents", href: "#incidents", icon: Activity, count: 2 },
      { label: "Dashboard", href: "#dashboard", icon: LayoutDashboard },
    ],
  },
  {
    label: "Operate",
    items: [
      { label: "On-call", href: "#on-call", icon: CalendarClock },
      { label: "Alert rules", href: "#alerts", icon: BellRing },
      { label: "Status pages", href: "#status", icon: Radio },
      { label: "Postmortems", href: "#postmortems", icon: FileText },
    ],
  },
];

const DEFAULT_TEAMS = [
  { name: "Payments", href: "#t-payments", color: "#5e6ad2" },
  { name: "Platform", href: "#t-platform", color: "#26b5ce" },
  { name: "Growth", href: "#t-growth", color: "#f2994a" },
];

export function AppSidebar({
  workspace = { name: "Acme" },
  groups = DEFAULT_GROUPS,
  activeHref = "#incidents",
  teams = DEFAULT_TEAMS,
  primaryAction = { label: "New incident" },
  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 link = (item: { href: string }, active: boolean, content: ReactNode) => (
    <a
      href={item.href}
      aria-current={active ? "page" : undefined}
      onClick={() => setOpen(false)}
      className={cn(
        "da-focus da-transition flex h-8 items-center gap-2.5 rounded-da-sm px-2 text-[13px]",
        active ? "bg-da-fg/[0.07] font-medium text-da-fg" : "text-da-muted-fg hover:bg-da-fg/[0.04] hover:text-da-fg",
      )}
    >
      {content}
    </a>
  );

  const panel = (
    <div className="flex h-full flex-col gap-4 p-3">
      <div className="flex items-center gap-1">
        <button type="button" className="da-focus da-transition flex min-w-0 flex-1 items-center gap-2 rounded-da-sm px-2 py-1.5 text-left hover:bg-da-fg/[0.04]">
          <span className="grid size-5 shrink-0 place-items-center rounded-[5px] bg-da-primary text-[10px] font-semibold text-da-primary-fg">
            {workspace.initials ?? workspace.name.slice(0, 1)}
          </span>
          <span className="truncate text-[13px] font-medium">{workspace.name}</span>
          <ChevronsUpDown aria-hidden className="ml-auto size-3.5 text-da-muted-fg" />
        </button>
        <button type="button" aria-label="Search" className="da-focus da-transition grid size-7 place-items-center rounded-da-sm text-da-muted-fg hover:bg-da-fg/[0.04] hover:text-da-fg">
          <Search aria-hidden className="size-4" />
        </button>
      </div>

      <button
        type="button"
        onClick={primaryAction.onClick}
        className="da-focus da-transition da-stroke flex h-8 items-center gap-2 rounded-da-md bg-da-surface px-2.5 text-[13px] font-medium text-da-surface-fg shadow-da-sm hover:bg-da-muted"
      >
        <Plus aria-hidden className="size-4" />
        {primaryAction.label}
        <kbd aria-hidden className="ml-auto font-da-mono text-[10px] text-da-muted-fg">
          C
        </kbd>
      </button>

      <nav aria-label="Sidebar" className="-mx-1 flex flex-1 flex-col gap-5 overflow-y-auto px-1">
        {groups.map((group, gi) => (
          <div key={group.label ?? gi}>
            {group.label && <p className="px-2 pb-1 text-[11px] font-medium text-da-muted-fg">{group.label}</p>}
            <ul className="flex flex-col gap-px">
              {group.items.map((item) => {
                const Icon = item.icon;
                return (
                  <li key={item.href}>
                    {link(
                      item,
                      item.href === activeHref,
                      <>
                        <Icon aria-hidden className="size-4 shrink-0" />
                        <span className="flex-1 truncate">{item.label}</span>
                        {item.count !== undefined && (
                          <span className="font-da-mono text-[11px] text-da-muted-fg">
                            {item.count}
                            <span className="sr-only"> items</span>
                          </span>
                        )}
                      </>,
                    )}
                  </li>
                );
              })}
            </ul>
          </div>
        ))}
        {teams.length > 0 && (
          <div>
            <p className="px-2 pb-1 text-[11px] font-medium text-da-muted-fg">Teams</p>
            <ul className="flex flex-col gap-px">
              {teams.map((t) => (
                <li key={t.href}>
                  {link(
                    t,
                    t.href === activeHref,
                    <>
                      <span aria-hidden className="size-2.5 rounded-[3px]" style={{ backgroundColor: t.color }} />
                      <span className="truncate">{t.name}</span>
                    </>,
                  )}
                </li>
              ))}
            </ul>
          </div>
        )}
      </nav>

      <div className="flex flex-col gap-px">
        {link(
          { href: "#settings" },
          activeHref === "#settings",
          <>
            <Settings aria-hidden className="size-4" />
            Settings
          </>,
        )}
      </div>
    </div>
  );

  return (
    <div className={cn("flex min-h-full bg-da-bg text-da-fg", className)}>
      <aside className="sticky top-0 hidden h-dvh w-60 shrink-0 border-r border-da-border bg-da-surface-2/50 lg:block">{panel}</aside>

      {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="relative h-full w-64 max-w-[85vw] border-r border-da-border bg-da-surface shadow-da-lg transition-transform duration-(--da-duration-slow) ease-da starting:-translate-x-full motion-reduce:transition-none"
          >
            {panel}
          </aside>
        </div>
      )}

      <div className="flex min-w-0 flex-1 flex-col">
        <div className="flex h-12 items-center gap-2 border-b border-da-border px-3 lg:hidden">
          <button
            type="button"
            aria-label="Open menu"
            aria-expanded={open}
            aria-controls={drawerId}
            onClick={() => setOpen(true)}
            className="da-focus da-transition grid size-8 place-items-center rounded-da-sm text-da-muted-fg hover:bg-da-fg/5 hover:text-da-fg"
          >
            <PanelLeft aria-hidden className="size-4" />
          </button>
          <span className="text-[13px] font-medium">{workspace.name}</span>
        </div>
        <main className="flex-1">{children}</main>
      </div>
    </div>
  );
}

export default AppSidebar;

modules/minimal/layout/app-sidebar/index.tsx

Props

PropTypeDefaultDescription
childrenReactNode—Page content (rendered in <main>).
workspace{ name: string; initials?: string }AcmeWorkspace switcher label.
groupsAppSidebarGroup[]—{ label?, items: { label, href, icon: LucideIcon, count? }[] }[].
activeHrefstring"#incidents"Current href (aria-current=page); pass usePathname() in Next.js.
teams{ name: string; href: string; color: string }[]—Team shortcuts; empty array hides the group.
primaryAction{ label: string; onClick?: () => void }New incidentButton under the workspace switcher.
classNamestring—Classes on the root.

Other sidebar variants in Minimal

Sidebar in other art directions

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.

BrutalistSidebar

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.

BrutalistSidebar

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.

BrutalistSidebar

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.

BrutalistSidebar

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.

GlassSidebar

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.

GlassSidebar

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.

GlassSidebar

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.

GlassSidebar

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.

Mono CleanSidebar

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.

Mono CleanSidebar

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.

Mono CleanSidebar

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.

Mono CleanSidebar

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.

Neo CorporateSidebar

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.

Neo CorporateSidebar

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.

Neo CorporateSidebar

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.

Neo CorporateSidebar

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.

Organic SoftSidebar

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.

Organic SoftSidebar

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.

Organic SoftSidebar

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.

Organic SoftSidebar

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.

Soft FlatSidebar

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.

Soft FlatSidebar

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.

Soft FlatSidebar

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.

Soft FlatSidebar