# Sidebar Nested Sections — React Sidebar Component

> A sidebar of collapsible groups with spring height animation; one active indicator bar slides between links, even across groups, along a tree guide line.

- Category: [Sidebars](https://pixelpapa.com/components/sidebars.md)
- Tags: sidebar, nested, collapsible, layoutid, navigation
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/sidebars/sidebar-nested-sections

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/sidebar-nested-sections.json"
```

The file is written to `components/pixelpapa/sidebar-nested-sections.tsx`. To install manually, run `npm install lucide-react motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { BookOpen, ChevronRight, Code, CreditCard, Rocket, ShieldCheck, type LucideIcon } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useId, useState } from "react";
import { cn } from "@/lib/utils";

export type NavSection = { title: string; icon: LucideIcon; items: string[] };

type NestedSidebarProps = {
  sections: NavSection[];
  active: string;
  onNavigate: (item: string) => void;
  defaultOpen?: string[];
  heading?: string;
  className?: string;
};

export function NestedSidebar({ sections, active, onNavigate, defaultOpen, heading, className }: NestedSidebarProps) {
  const uid = useId();
  const [open, setOpen] = useState<string[]>(defaultOpen ?? sections.map((s) => s.title));

  return (
    <aside className={cn("flex w-64 shrink-0 flex-col border-r border-border bg-card", className)}>
      {heading && <p className="px-6 pt-6 pb-2 text-sm font-semibold text-foreground">{heading}</p>}
      <nav aria-label={heading ?? "Sections"} className="flex-1 space-y-1 overflow-y-auto p-3">
        {sections.map(({ title, icon: Icon, items }, s) => {
          const isOpen = open.includes(title);
          const holdsActive = items.includes(active);
          return (
            <div key={title}>
              <button
                type="button"
                aria-expanded={isOpen}
                aria-controls={`${uid}-${s}`}
                onClick={() => setOpen((prev) => (isOpen ? prev.filter((t) => t !== title) : [...prev, title]))}
                className="group flex h-9 w-full items-center gap-2.5 rounded-lg px-3 text-sm font-medium text-foreground outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/60"
              >
                <Icon className={cn("size-4 transition-colors", holdsActive ? "text-primary" : "text-muted-foreground")} />
                <span className="flex-1 text-left">{title}</span>
                {!isOpen && holdsActive && <span className="size-1.5 rounded-full bg-primary" />}
                <motion.span animate={{ rotate: isOpen ? 90 : 0 }} transition={{ type: "spring", stiffness: 500, damping: 30 }} className="text-muted-foreground/70">
                  <ChevronRight className="size-3.5" />
                </motion.span>
              </button>
              <AnimatePresence initial={false}>
                {isOpen && (
                  <motion.div
                    id={`${uid}-${s}`}
                    initial={{ height: 0, opacity: 0 }}
                    animate={{ height: "auto", opacity: 1 }}
                    exit={{ height: 0, opacity: 0 }}
                    transition={{ type: "spring", stiffness: 400, damping: 38, opacity: { duration: 0.15 } }}
                    className="overflow-hidden"
                  >
                    <ul className="relative mt-0.5 mb-1.5 ml-5 border-l border-border py-0.5">
                      {items.map((item) => {
                        const on = item === active;
                        return (
                          <li key={item} className="relative">
                            {on && (
                              <motion.span
                                layoutId={`sidebar-nested-sections-${uid}`}
                                transition={{ type: "spring", stiffness: 440, damping: 36 }}
                                className="absolute inset-y-1 -left-px w-0.5 rounded-full bg-primary"
                              />
                            )}
                            <a
                              href="#"
                              aria-current={on ? "page" : undefined}
                              onClick={(e) => {
                                e.preventDefault();
                                onNavigate(item);
                              }}
                              className={cn(
                                "ml-2 flex h-8 items-center rounded-md px-2.5 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring/60",
                                on ? "bg-primary/[0.07] font-medium text-primary" : "text-muted-foreground hover:bg-muted hover:text-foreground",
                              )}
                            >
                              {item}
                            </a>
                          </li>
                        );
                      })}
                    </ul>
                  </motion.div>
                )}
              </AnimatePresence>
            </div>
          );
        })}
      </nav>
    </aside>
  );
}

const sections: NavSection[] = [
  { title: "Getting started", icon: Rocket, items: ["Introduction", "Quickstart", "Core concepts"] },
  { title: "Payments", icon: CreditCard, items: ["Accept a payment", "Refunds", "Disputes", "Payouts"] },
  { title: "API reference", icon: Code, items: ["Authentication", "Pagination", "Webhooks", "Errors"] },
  { title: "Security", icon: ShieldCheck, items: ["API keys", "Audit log"] },
  { title: "Guides", icon: BookOpen, items: ["Migrating from v3", "Testing in sandbox"] },
];

export default function SidebarNestedSectionsDemo() {
  const [active, setActive] = useState("Refunds");
  const group = sections.find((s) => s.items.includes(active))?.title;

  return (
    <div className="flex min-h-screen w-full bg-background">
      <NestedSidebar heading="Ledgerly Docs" sections={sections} active={active} onNavigate={setActive} defaultOpen={["Getting started", "Payments", "API reference"]} />
      <main className="min-w-0 flex-1 px-8 py-10 lg:px-16">
        <p className="text-sm font-medium text-primary">{group}</p>
        <motion.div key={active} initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
          <h1 className="mt-2 text-3xl font-semibold tracking-tight text-foreground">{active}</h1>
          <p className="mt-3 max-w-xl leading-relaxed text-muted-foreground">
            Everything you need to work with {active.toLowerCase()} in Ledgerly: the request shape, the events we emit, and the edge cases worth testing before you go live.
          </p>
          <div className="mt-8 max-w-2xl space-y-3">
            {[92, 100, 84, 96, 60].map((w, i) => (
              <span key={i} className="block h-3 rounded-full bg-muted" style={{ width: `${w}%` }} />
            ))}
          </div>
          <div className="mt-8 max-w-2xl space-y-2.5 rounded-2xl border border-border bg-card p-5">
            {[40, 64, 52, 30].map((w, i) => (
              <span key={i} className={cn("block h-2.5 rounded-full", i === 0 ? "bg-primary/30" : "bg-muted")} style={{ width: `${w}%`, marginLeft: i === 0 || i === 3 ? 0 : 16 }} />
            ))}
          </div>
        </motion.div>
      </main>
    </div>
  );
}
```

## AI prompt

```text
Add the "Sidebar Nested Sections" component from PixelPapa UI to this project.

What it is: A sidebar of collapsible groups with spring height animation; one active indicator bar slides between links, even across groups, along a tree guide line.
Category: Sidebars. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/sidebar-nested-sections.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/sidebar-nested-sections.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/sidebar-nested-sections" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More sidebars

- [Sidebar Collapsible Rail](https://pixelpapa.com/components/sidebars/sidebar-collapsible-rail.md): An app sidebar that springs between a full panel and a 64px icon rail; labels fade out, and in rail mode each icon shows a tooltip on hover or focus.
- [Sidebar Floating Inset](https://pixelpapa.com/components/sidebars/sidebar-floating-inset.md): A rounded sidebar that floats inside the page margin, with a search field that filters links live (rows reflow with layout animation), count badges and a storage meter.
- [Sidebar Workspace Switcher](https://pixelpapa.com/components/sidebars/sidebar-workspace-switcher.md) (Pro): A sidebar with a workspace switcher on top whose menu springs open and rolls the new workspace into place, and a user card at the bottom with its own pop-up menu.
