Skip to content

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.

minimal/layout/sidebar-rail
Open ↗

Source

"use client";

import { useEffect, useRef, useState, type ReactNode } from "react";
import { Activity, BellRing, CalendarClock, ChevronsLeft, ChevronsRight, FileText, Inbox, Radio, Settings, type LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";

export interface SidebarRailItem {
  label: string;
  href: string;
  icon: LucideIcon;
  badge?: number;
}

export interface SidebarRailProps {
  items?: SidebarRailItem[];
  activeHref?: string;
  /** Start collapsed to icons. */
  defaultCollapsed?: boolean;
  /** Controlled state (pair with onCollapsedChange to persist it). */
  collapsed?: boolean;
  onCollapsedChange?: (collapsed: boolean) => void;
  children?: ReactNode;
  className?: string;
}

const DEFAULT_ITEMS: SidebarRailItem[] = [
  { label: "Inbox", href: "#inbox", icon: Inbox, badge: 4 },
  { label: "Incidents", href: "#incidents", icon: Activity, badge: 2 },
  { 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 },
];

function Row({ it, collapsed, activeHref }: { it: SidebarRailItem; collapsed: boolean; activeHref: string }) {
  const active = it.href === activeHref;
  return (
    <a
      href={it.href}
      aria-current={active ? "page" : undefined}
      aria-label={collapsed ? it.label : undefined}
      className={cn(
        "da-focus da-transition group/item relative flex h-9 items-center gap-3 rounded-da-md px-2.5 text-[13px]",
        active ? "bg-da-muted font-medium text-da-fg" : "text-da-muted-fg hover:bg-da-fg/5 hover:text-da-fg",
      )}
    >
      <it.icon aria-hidden className="size-4 shrink-0" />
      <span className={cn("truncate", collapsed ? "sr-only" : "max-md:sr-only")}>{it.label}</span>
      {it.badge !== undefined && (
        <span
          className={cn(
            "rounded-da-pill bg-da-primary font-da-mono text-[10px] text-da-primary-fg",
            collapsed
              ? "absolute top-1 right-1 grid size-4 place-items-center"
              : "max-md:absolute max-md:top-1 max-md:right-1 max-md:grid max-md:size-4 max-md:place-items-center md:ml-auto md:px-1.5",
          )}
        >
          {it.badge}
        </span>
      )}
      {
        <span
          aria-hidden
          className={cn(
            !collapsed && "md:hidden",
            "pointer-events-none absolute left-full z-20 ml-3 rounded-da-sm bg-da-fg px-2 py-1 text-[12px] whitespace-nowrap text-da-bg opacity-0 shadow-da-md transition-opacity duration-(--da-duration) group-hover/item:opacity-100 group-focus-visible/item:opacity-100",
          )}
        >
          {it.label}
        </span>
      }
    </a>
  );
}

/** Sidebar that collapses between 224px (icons + labels) and a 56px icon rail with hover/focus tooltips. ⌘B toggles; controlled or uncontrolled. */
export function SidebarRail({
  items = DEFAULT_ITEMS,
  activeHref = "#incidents",
  defaultCollapsed = false,
  collapsed: controlled,
  onCollapsedChange,
  children,
  className,
}: SidebarRailProps) {
  const [inner, setInner] = useState(defaultCollapsed);
  const collapsed = controlled ?? inner;
  const toggleRef = useRef<() => void>(() => {});
  const toggle = () => {
    setInner(!collapsed);
    onCollapsedChange?.(!collapsed);
  };
  useEffect(() => {
    toggleRef.current = toggle;
  });

  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "b") {
        e.preventDefault();
        toggleRef.current();
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);

  return (
    <div className={cn("flex min-h-dvh bg-da-bg text-da-fg", className)}>
      <aside
        className={cn(
          "sticky top-0 flex h-dvh shrink-0 flex-col border-r border-da-border bg-da-surface-2 p-2 transition-[width] duration-(--da-duration-slow) ease-da motion-reduce:transition-none",
          collapsed ? "w-14" : "w-14 md:w-56",
        )}
      >
        <div className="flex h-10 items-center gap-2 px-2.5">
          <svg aria-hidden viewBox="0 0 24 24" className="size-5 shrink-0 text-da-primary" fill="none">
            <path d="M3 12h4l2.5-6 5 12 2.5-6H21" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
          <span className={cn("text-sm font-semibold", collapsed ? "sr-only" : "hidden md:inline")}>Tracewise</span>
        </div>
        <nav aria-label="Main" className="mt-3 flex-1 space-y-0.5">
          {items.map((it) => (
            <Row key={it.href} it={it} collapsed={collapsed} activeHref={activeHref} />
          ))}
        </nav>
        <Row it={{ label: "Settings", href: "#settings", icon: Settings }} collapsed={collapsed} activeHref={activeHref} />
        <button
          type="button"
          onClick={toggle}
          aria-pressed={collapsed}
          aria-keyshortcuts="Meta+B"
          className="da-focus da-transition mt-1 hidden h-9 items-center gap-3 rounded-da-md px-2.5 text-[13px] text-da-muted-fg hover:bg-da-fg/5 hover:text-da-fg md:flex"
        >
          {collapsed ? <ChevronsRight aria-hidden className="size-4 shrink-0" /> : <ChevronsLeft aria-hidden className="size-4 shrink-0" />}
          <span className={cn(collapsed && "sr-only")}>Collapse sidebar</span>
          {!collapsed && <kbd className="ml-auto font-da-mono text-[10px]">⌘B</kbd>}
        </button>
      </aside>
      <main className="min-w-0 flex-1">{children}</main>
    </div>
  );
}

export default SidebarRail;

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

Props

PropTypeDefaultDescription
items{ label, href, icon, badge? }[]—Nav items.
activeHrefstring"#incidents"Current page.
defaultCollapsedbooleanfalseInitial state.
collapsed / onCollapsedChangeboolean / fn—Controlled state (persist it yourself).
childrenReactNode—Page content.
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