# Sidebar Collapsible Rail — React Sidebar Component

> 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.

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

## Install

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

The file is written to `components/pixelpapa/sidebar-collapsible-rail.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 { ChartColumn, CreditCard, Inbox, LayoutDashboard, PanelLeft, Settings, Truck, Users, type LucideIcon } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useId, useState } from "react";
import { cn } from "@/lib/utils";

export type RailItem = { label: string; icon: LucideIcon; href?: string; badge?: number };

type CollapsibleSidebarProps = {
  items: RailItem[];
  footerItems?: RailItem[];
  brand: string;
  active: string;
  onNavigate?: (label: string) => void;
  /** Controlled state. Leave undefined to let the sidebar manage it. */
  collapsed?: boolean;
  defaultCollapsed?: boolean;
  onCollapsedChange?: (collapsed: boolean) => void;
  className?: string;
};

export function CollapsibleSidebar({
  items,
  footerItems = [],
  brand,
  active,
  onNavigate,
  collapsed: controlled,
  defaultCollapsed = false,
  onCollapsedChange,
  className,
}: CollapsibleSidebarProps) {
  const uid = useId();
  const reduce = useReducedMotion();
  const [inner, setInner] = useState(defaultCollapsed);
  const [tip, setTip] = useState<string | null>(null);
  const collapsed = controlled ?? inner;
  const spring = reduce ? { duration: 0 } : ({ type: "spring", stiffness: 340, damping: 34 } as const);

  function toggle() {
    setInner(!collapsed);
    onCollapsedChange?.(!collapsed);
  }

  const label = (text: string, extra?: string) => (
    <motion.span
      initial={false}
      animate={{ opacity: collapsed ? 0 : 1, x: collapsed ? -6 : 0 }}
      transition={{ duration: 0.18, delay: collapsed || reduce ? 0 : 0.08 }}
      className={cn("truncate whitespace-nowrap", extra)}
    >
      {text}
    </motion.span>
  );

  const link = ({ label: text, icon: Icon, href, badge }: RailItem) => {
    const on = text === active;
    return (
      <li key={text} className="relative">
        <a
          href={href ?? "#"}
          aria-current={on ? "page" : undefined}
          aria-label={collapsed ? text : undefined}
          onClick={(e) => {
            if (!href) e.preventDefault();
            onNavigate?.(text);
          }}
          onPointerEnter={() => setTip(text)}
          onPointerLeave={() => setTip(null)}
          onFocus={() => setTip(text)}
          onBlur={() => setTip(null)}
          className={cn(
            "relative flex h-10 items-center gap-3 overflow-hidden rounded-xl px-3 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring/60",
            on ? "font-medium text-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground",
          )}
        >
          {on && (
            <motion.span
              layoutId={`sidebar-collapsible-rail-${uid}`}
              transition={{ type: "spring", stiffness: 420, damping: 36 }}
              className="absolute inset-0 rounded-xl bg-primary/10 ring-1 ring-primary/15 ring-inset"
            />
          )}
          <span className="relative shrink-0">
            <Icon className={cn("size-4", on && "text-primary")} />
            {badge && collapsed ? <span className="absolute -top-1 -right-1 size-2 rounded-full bg-primary ring-2 ring-card" /> : null}
          </span>
          {label(text, "relative flex-1")}
          {badge ? label(String(badge), "relative rounded-full bg-primary/10 px-1.5 py-0.5 text-[11px] font-semibold text-primary tabular-nums") : null}
        </a>
        <AnimatePresence>
          {collapsed && tip === text && (
            <motion.span
              role="tooltip"
              initial={{ opacity: 0, x: -6, scale: 0.94 }}
              animate={{ opacity: 1, x: 0, scale: 1 }}
              exit={{ opacity: 0, transition: { duration: 0.08 } }}
              transition={{ type: "spring", stiffness: 520, damping: 30 }}
              className="pointer-events-none absolute top-1/2 left-full z-30 ml-3 -translate-y-1/2 rounded-lg bg-foreground px-2.5 py-1.5 text-xs font-medium whitespace-nowrap text-background shadow-lg"
            >
              {text}
              {badge ? <span className="ml-1.5 opacity-60">{badge}</span> : null}
            </motion.span>
          )}
        </AnimatePresence>
      </li>
    );
  };

  return (
    <motion.aside
      initial={false}
      animate={{ width: collapsed ? 64 : 248 }}
      transition={spring}
      className={cn("relative z-10 flex shrink-0 flex-col border-r border-border bg-card", className)}
    >
      <div className="flex h-16 items-center gap-3 overflow-hidden px-4">
        <span className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-primary text-sm font-bold text-primary-foreground">{brand[0]}</span>
        {label(brand, "text-sm font-semibold text-foreground")}
      </div>
      <nav aria-label="Main" className="flex flex-1 flex-col justify-between p-3">
        <ul className="space-y-1">{items.map(link)}</ul>
        <ul className="space-y-1">{footerItems.map(link)}</ul>
      </nav>
      <button
        type="button"
        aria-expanded={!collapsed}
        aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
        onClick={toggle}
        className="absolute top-5 -right-3 flex size-6 items-center justify-center rounded-full border border-border bg-card text-muted-foreground shadow-sm outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/60"
      >
        <PanelLeft className="size-3" />
      </button>
    </motion.aside>
  );
}

const nav: RailItem[] = [
  { label: "Overview", icon: LayoutDashboard },
  { label: "Shipments", icon: Truck, badge: 12 },
  { label: "Inbox", icon: Inbox, badge: 3 },
  { label: "Reports", icon: ChartColumn },
  { label: "Customers", icon: Users },
];
const footer: RailItem[] = [
  { label: "Billing", icon: CreditCard },
  { label: "Settings", icon: Settings },
];
const cards = [
  { k: "In transit", v: "148", d: "+12 today" },
  { k: "Delivered this week", v: "1,204", d: "98.2% on time" },
  { k: "Exceptions", v: "7", d: "3 need a reply" },
];

export default function SidebarCollapsibleRailDemo() {
  const [active, setActive] = useState("Shipments");
  const [collapsed, setCollapsed] = useState(false);

  return (
    <div className="flex min-h-screen w-full bg-background">
      <CollapsibleSidebar brand="Halden Freight" items={nav} footerItems={footer} active={active} onNavigate={setActive} collapsed={collapsed} onCollapsedChange={setCollapsed} />
      <main className="min-w-0 flex-1 p-8 lg:p-10">
        <p className="text-sm text-muted-foreground">Sidebar is {collapsed ? "collapsed to a rail — hover an icon" : "expanded"}</p>
        <h1 className="mt-1 text-2xl font-semibold tracking-tight text-foreground">{active}</h1>
        <div className="mt-8 grid gap-4 sm:grid-cols-3">
          {cards.map((c) => (
            <div key={c.k} className="rounded-2xl border border-border bg-card p-5">
              <p className="text-sm text-muted-foreground">{c.k}</p>
              <p className="mt-2 text-3xl font-semibold tracking-tight text-card-foreground tabular-nums">{c.v}</p>
              <p className="mt-1 text-xs text-muted-foreground">{c.d}</p>
            </div>
          ))}
        </div>
        <div className="mt-4 space-y-3 rounded-2xl border border-border bg-card p-5">
          {[72, 56, 64, 48, 60, 40].map((w, i) => (
            <div key={i} className="flex items-center gap-4">
              <span className="size-8 rounded-lg bg-muted" />
              <span className="h-2.5 rounded-full bg-muted" style={{ width: `${w}%` }} />
              <span className="ml-auto h-2.5 w-14 rounded-full bg-muted" />
            </div>
          ))}
        </div>
      </main>
    </div>
  );
}
```

## AI prompt

```text
Add the "Sidebar Collapsible Rail" component from PixelPapa UI to this project.

What it is: 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.
Category: Sidebars. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/sidebar-collapsible-rail.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/sidebar-collapsible-rail.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/sidebar-collapsible-rail" 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 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 Nested Sections](https://pixelpapa.com/components/sidebars/sidebar-nested-sections.md): A sidebar of collapsible groups with spring height animation; one active indicator bar slides between links, even across groups, along a tree guide line.
- [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.
