Skip to content
Dethink Components

ComponentsSidebar

Sidebar

Organize app navigation in a sidebar that can collapse.

On this page

Use this component from the local workspace package. Follow the setup guide first. A public npm package and registry are not available yet.

View registry files

Import the component into your page or component file.

Usage
import {
  Sidebar,
  SidebarProvider,
  SidebarMenuLink,
} from "@dethink/components";

Try the examples, then open the code to use them in your app.

Dashboard navigation

A labelled nav landmark with grouped links, current route state, badges, shortcuts, trigger, rail, and content inset.

Main content keeps its own layout. Sidebar only owns navigation.
Show sourceexamples/sidebar/basic.tsx
examples/sidebar/basic.tsx
"use client";

import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuItem,
  SidebarMenuLink,
  SidebarProvider,
  SidebarRail,
} from "@dethink/components";
import { BarChart3, LayoutDashboard } from "lucide-react";

export function SidebarBasic() {
  return (
    <SidebarProvider>
      <div className="border-border bg-background flex h-80 overflow-hidden rounded-lg border">
        <Sidebar aria-label="Product navigation">
          <SidebarHeader>
            <div className="flex min-w-0 items-center justify-between gap-2">
              <div className="min-w-0">
                <div className="text-foreground truncate text-sm font-semibold">
                  Dethink Cloud
                </div>
                <div className="text-muted-foreground truncate text-xs">
                  Operations
                </div>
              </div>
            </div>
          </SidebarHeader>
          <SidebarContent>
            <SidebarGroup>
              <SidebarGroupLabel>Workspace</SidebarGroupLabel>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuLink
                      current
                      href="/overview"
                      icon={<LayoutDashboard aria-hidden="true" />}
                    >
                      Overview
                    </SidebarMenuLink>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuLink
                      badge="12"
                      href="/analytics"
                      icon={<BarChart3 aria-hidden="true" />}
                      shortcut="G A"
                    >
                      Analytics
                    </SidebarMenuLink>
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
          </SidebarContent>
          <SidebarRail />
        </Sidebar>
        <SidebarInset className="p-6">
          <div className="border-border bg-muted/30 text-muted-foreground rounded-md border p-4 text-sm">
            Main content keeps its own layout. Sidebar only owns navigation.
          </div>
        </SidebarInset>
      </div>
    </SidebarProvider>
  );
}

Collapsed rail

Icon rail mode preserves accessible names while trading visual labels for workspace width.

Collapsed labels remain in the accessibility tree while visual space stays compact.
Show sourceexamples/sidebar/collapsed.tsx
examples/sidebar/collapsed.tsx
"use client";

import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarInset,
  SidebarMenu,
  SidebarMenuItem,
  SidebarMenuLink,
  SidebarProvider,
  SidebarRail,
} from "@dethink/components";
import { FileBarChart, FolderKanban, Inbox } from "lucide-react";

const collapsedItems = [
  { href: "/inbox", icon: Inbox, label: "Inbox" },
  { href: "/projects", icon: FolderKanban, label: "Projects" },
  { href: "/reports", icon: FileBarChart, label: "Reports" },
];

export function SidebarCollapsed() {
  return (
    <SidebarProvider defaultCollapsed motion="subtle">
      <div className="border-border bg-background flex h-72 overflow-hidden rounded-lg border">
        <Sidebar aria-label="Collapsed navigation" variant="bordered">
          <SidebarContent>
            <SidebarGroup>
              <SidebarGroupLabel>Rail</SidebarGroupLabel>
              <SidebarGroupContent>
                <SidebarMenu>
                  {collapsedItems.map((item, index) => {
                    const Icon = item.icon;

                    return (
                      <SidebarMenuItem key={item.href}>
                        <SidebarMenuLink
                          current={index === 0}
                          href={item.href}
                          icon={<Icon aria-hidden="true" />}
                        >
                          {item.label}
                        </SidebarMenuLink>
                      </SidebarMenuItem>
                    );
                  })}
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
          </SidebarContent>
          <SidebarRail />
        </Sidebar>
        <SidebarInset className="text-muted-foreground p-5 text-sm">
          Collapsed labels remain in the accessibility tree while visual space
          stays compact.
        </SidebarInset>
      </div>
    </SidebarProvider>
  );
}

Mobile menu

A contained mobile navigation handoff with explicit open and close states for embedded workspace previews.

Mobile workspace
Open the menu and choose a workspace link.
Show sourceexamples/sidebar/mobile-drawer.tsx
examples/sidebar/mobile-drawer.tsx
"use client";

import {
  SidebarContent,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarMenu,
  SidebarMenuItem,
  SidebarMenuLink,
  SidebarMobileTrigger,
  SidebarProvider,
} from "@dethink/components";
import { Activity, Home, Menu, Settings, X } from "lucide-react";
import { useState } from "react";

const mobileLinks = [
  { href: "#overview", icon: Home, label: "Overview" },
  { href: "#incidents", icon: Activity, label: "Incidents" },
  { href: "#settings", icon: Settings, label: "Settings" },
];

export function SidebarMobileDrawer() {
  const [open, setOpen] = useState(false);

  return (
    <SidebarProvider mobileOpen={open} onMobileOpenChange={setOpen}>
      <div className="border-border bg-background overflow-hidden rounded-lg border">
        <div className="flex items-center justify-between gap-4 p-6">
          <div className="flex min-w-0 items-center gap-3">
            <span
              aria-hidden="true"
              className="border-border bg-muted text-muted-foreground flex size-9 shrink-0 items-center justify-center rounded-md border"
            >
              <Home className="size-4" />
            </span>
            <div className="min-w-0">
              <div className="text-foreground text-sm font-semibold">
                Mobile workspace
              </div>
              <div className="text-muted-foreground text-xs">
                Open the menu and choose a workspace link.
              </div>
            </div>
          </div>
          <SidebarMobileTrigger
            aria-controls="sidebar-mobile-menu-preview"
            className="size-9"
            closeLabel="Close menu"
            openLabel="Open menu"
          >
            {open ? (
              <X aria-hidden="true" className="size-4" />
            ) : (
              <Menu aria-hidden="true" className="size-4" />
            )}
          </SidebarMobileTrigger>
        </div>

        <div
          hidden={!open}
          id="sidebar-mobile-menu-preview"
          data-state={open ? "open" : "closed"}
          className="border-border bg-muted/30 data-[state=open]:motion-safe:animate-sidebar-menu-open border-t p-3 data-[state=open]:motion-reduce:animate-none"
        >
          <SidebarContent>
            <SidebarGroup>
              <SidebarGroupLabel>Workspace</SidebarGroupLabel>
              <SidebarGroupContent>
                <SidebarMenu>
                  {mobileLinks.map((item, index) => {
                    const Icon = item.icon;

                    return (
                      <SidebarMenuItem key={item.href}>
                        <SidebarMenuLink
                          current={index === 0}
                          href={item.href}
                          icon={<Icon aria-hidden="true" />}
                          onClick={() => setOpen(false)}
                        >
                          {item.label}
                        </SidebarMenuLink>
                      </SidebarMenuItem>
                    );
                  })}
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
          </SidebarContent>
        </div>
      </div>
    </SidebarProvider>
  );
}

Examples that combine components for common tasks.

AI workspace navigation

A floating sidebar with nested workflow navigation, live badges, disabled items, motion preset state, and dashboard content.

38 active runs

Navigation state stays visible while the workspace updates.

92% eval pass rate

Badges and descriptions fit dense AI-native surfaces.

Show sourceexamples/sidebar/recipe-ai-workspace.tsx
examples/sidebar/recipe-ai-workspace.tsx
"use client";

import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupTrigger,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuAction,
  SidebarMenuItem,
  SidebarMenuLink,
  SidebarProvider,
  SidebarRail,
} from "@dethink/components";
import { Activity, Gauge, Settings2, Sparkles } from "lucide-react";

export function SidebarRecipeAiWorkspace() {
  return (
    <SidebarProvider variant="floating" motion="expressive">
      <div className="border-border bg-muted/30 flex h-[28rem] overflow-hidden rounded-lg border">
        <Sidebar aria-label="AI workspace navigation">
          <SidebarHeader>
            <div className="flex min-w-0 items-center justify-between gap-2">
              <div className="min-w-0">
                <div className="text-foreground truncate text-sm font-semibold">
                  Assist Studio
                </div>
                <div className="text-muted-foreground truncate text-xs">
                  Model operations
                </div>
              </div>
            </div>
          </SidebarHeader>
          <SidebarContent>
            <SidebarGroup collapsible defaultOpen>
              <SidebarGroupTrigger>Workflows</SidebarGroupTrigger>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuLink
                      current
                      badge="Live"
                      description="Streaming review queue"
                      href="#runs"
                      icon={<Sparkles aria-hidden="true" />}
                    >
                      Agent runs
                    </SidebarMenuLink>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuLink
                      href="#evaluations"
                      icon={<Activity aria-hidden="true" />}
                    >
                      Evaluations
                    </SidebarMenuLink>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuLink
                      disabled
                      href="#billing"
                      icon={<Gauge aria-hidden="true" />}
                    >
                      Cost controls
                    </SidebarMenuLink>
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
          </SidebarContent>
          <SidebarFooter>
            <div className="flex items-center justify-between gap-2">
              <span className="text-muted-foreground truncate text-sm">
                Team quota
              </span>
              <SidebarMenuAction aria-label="Open quota settings">
                <Settings2 aria-hidden="true" />
              </SidebarMenuAction>
            </div>
          </SidebarFooter>
          <SidebarRail />
        </Sidebar>
        <SidebarInset className="p-6">
          <div className="grid gap-4 md:grid-cols-2">
            <div className="border-border bg-background rounded-md border p-4">
              <div className="text-foreground text-sm font-semibold">
                38 active runs
              </div>
              <p className="text-muted-foreground mt-2 text-sm">
                Navigation state stays visible while the workspace updates.
              </p>
            </div>
            <div className="border-border bg-background rounded-md border p-4">
              <div className="text-foreground text-sm font-semibold">
                92% eval pass rate
              </div>
              <p className="text-muted-foreground mt-2 text-sm">
                Badges and descriptions fit dense AI-native surfaces.
              </p>
            </div>
          </div>
        </SidebarInset>
      </div>
    </SidebarProvider>
  );
}

Sidebar is composable. The provider owns shared state; parts render semantic navigation, controls, drawers, and app-shell companions.

Sidebar props
PropWhat it doesDefault
SidebarProvider{ animate, collapsed, defaultCollapsed, onCollapsedChange, mobileOpen, defaultMobileOpen, onMobileOpenChange, side, variant, motion }Owns desktop collapsed state, mobile drawer state, side placement, visual variant, animation opt-out, and motion preset.Not set
Sidebarnav attributes + { side, variant }The labelled navigation landmark. Side and variant default from the provider but can be overridden.aria-label="Sidebar"
SidebarMenuLink{ href, current, active, disabled, external, asChild, icon, description, badge, shortcut, tooltip }Navigation item rendered as a real link by default, with aria-current and data-state hooks for current route styling. Current items render an animated selection indicator; collapsed items expose a CSS-only tooltip (string labels are used automatically, or pass tooltip), a badge dot, and an initial fallback when no icon is given.Not set
SidebarMenuAction{ label, showOnHover }Icon-sized secondary action. With showOnHover it positions itself at the end of the parent SidebarMenuItem row and reveals on row hover, focus-within, or its own focus.showOnHover: false
SidebarSeparatordiv attributesDecorative token-backed rule for dividing sidebar sections.Not set
SidebarGroup{ collapsible, open, defaultOpen, onOpenChange }Groups dense navigation sections; collapsible groups pair with SidebarGroupTrigger and hide content when closed.defaultOpen: true
SidebarTrigger / SidebarRailbutton propsAlternative desktop collapse controls with aria-expanded state. SidebarRail is the standard compact edge tab with a small Motion-powered chevron; it is a binary toggle, not a resize or drag handle. Use SidebarTrigger only when an inline control is intentional, not alongside the rail.toggle collapse
SidebarMobile / SidebarMobileTrigger{ label, closeButtonLabel, showCloseButton } / button propsMobile drawer surface and trigger. The drawer handles Escape, outside click, link activation close, and focus restore.label: "Sidebar navigation"
SidebarInset{ as?: "main" | "div" | "section" }Lightweight content companion for app shells that need Sidebar and main content to share a flex row."main"
animatebooleanEnables the CSS choreography and Motion edge-handle feedback. Set false to resolve every sidebar surface to motion none.true
motion"none" | "subtle" | "standard" | "expressive"Coordinates CSS width, disclosure, drawer, and menu transitions with the Motion-powered edge handle. Reduced-motion preferences disable transform-heavy handle movement."standard"