Sidebar 001

Animated vertical sidebar with spring hover highlight, animated active bar, collapsible groups, and drag-to-resize.

Installation

File Structure

sidebar-001.tsx

Usage

A full example showing sections, collapsible groups, the isNew badge, and an effects toggle:

"use client";

import { usePathname } from "next/navigation";
import {
  Sidebar001,
  Sidebar001Content,
  Sidebar001Header,
  Sidebar001Footer,
  Sidebar001Section,
  Sidebar001Group,
  Sidebar001Item,
  useSidebar001Effects,
} from "@/components/unlumen-ui/sidebar-001";
import { BookOpen, Layers } from "lucide-react";

const nav = [
  {
    label: "Getting Started",
    items: [
      { href: "/docs/introduction", label: "Introduction" },
      { href: "/docs/installation", label: "Installation" },
      { href: "/docs/theming", label: "Theming", isNew: true },
    ],
  },
];

const advanced = [
  { href: "/docs/animations", label: "Animations" },
  { href: "/docs/accessibility", label: "Accessibility" },
];

function EffectsToggle() {
  const { enabled, toggle } = useSidebar001Effects();
  return (
    <button
      onClick={toggle}
      className="text-xs text-muted-foreground hover:text-foreground transition-colors"
    >
      Effects: {enabled ? "on" : "off"}
    </button>
  );
}

export function AppSidebar() {
  const pathname = usePathname();

  return (
    <Sidebar001 defaultWidth={260}>
      <Sidebar001Header>
        <span className="text-sm font-semibold">Docs</span>
      </Sidebar001Header>

      <Sidebar001Content>
        {/* Flat section with a heading */}
        {nav.map((section) => (
          <Sidebar001Section key={section.label} label={section.label}>
            {section.items.map((item) => (
              <Sidebar001Item
                key={item.href}
                href={item.href}
                label={item.label}
                isActive={pathname === item.href}
                isNew={item.isNew}
              />
            ))}
          </Sidebar001Section>
        ))}

        {/* Collapsible group */}
        <Sidebar001Group label="Advanced" icon={<Layers />} defaultOpen>
          {advanced.map((item) => (
            <Sidebar001Item
              key={item.href}
              href={item.href}
              label={item.label}
              isActive={pathname === item.href}
            />
          ))}
        </Sidebar001Group>
      </Sidebar001Content>

      <Sidebar001Footer>
        <EffectsToggle />
      </Sidebar001Footer>
    </Sidebar001>
  );
}

Composition

The sidebar is built from composable pieces:

ComponentRole
Sidebar001Root — provides context, sets width, renders resize handle
Sidebar001HeaderFixed top slot
Sidebar001ContentScrollable body — place sections and groups here
Sidebar001SectionFlat list of items with an optional heading
Sidebar001GroupCollapsible group with animated open/close
Sidebar001ItemIndividual navigation link
Sidebar001FooterFixed bottom slot, above a border

Collapsible groups

Sidebar001Group wraps any number of items in an animated accordion. Use it when you want to collapse an entire category.

<Sidebar001Group label="Components" icon={<Layers />} defaultOpen>
  <Sidebar001Item href="/docs/button" label="Button" isActive={false} />
  <Sidebar001Item href="/docs/card" label="Card" isActive={false} />
</Sidebar001Group>

Pass icon to show a leading icon next to the label. Omit it for a compact chevron-only style.

New badge

Pass isNew to any item to show a small accent dot after the label:

<Sidebar001Item href="/docs/theming" label="Theming" isActive={false} isNew />

Effects toggle

Motion effects are stored in localStorage and opt-out by default. Use useSidebar001Effects to let users control it:

import { useSidebar001Effects } from "@/components/unlumen-ui/sidebar-001";

function EffectsToggle() {
  const { enabled, toggle } = useSidebar001Effects();
  return <button onClick={toggle}>Effects: {enabled ? "on" : "off"}</button>;
}

The hook must be called inside a <Sidebar001> tree.

Resize handle

The sidebar is draggable by default. Constrain or disable it via minWidth / maxWidth:

<Sidebar001 defaultWidth={240} minWidth={180} maxWidth={360}>
  ...
</Sidebar001>

API Reference

Sidebar001

PropTypeDefaultDescription
childrenReact.ReactNode-Sidebar contents — Header, Content, Footer.
className?string-Additional classes on the root aside element.
defaultEffectsEnabled?booleantrueInitial value for the motion effects toggle (persisted in localStorage).
defaultWidth?number240Initial sidebar width in px.
minWidth?number160Minimum drag width in px.
maxWidth?number400Maximum drag width in px.

Sidebar001Header

PropTypeDefaultDescription
children?React.ReactNode-Content rendered at the top of the sidebar.
className?string-Additional classes.

Sidebar001Content

PropTypeDefaultDescription
childrenReact.ReactNode-Scrollable body — typically Sidebar001Section and Sidebar001Group elements.
className?string-Additional classes on the scroll container.

Sidebar001Section

PropTypeDefaultDescription
label?React.ReactNode-Section heading shown above the group of items.
childrenReact.ReactNode-Sidebar001Item elements.
className?string-Additional classes.

Sidebar001Group

PropTypeDefaultDescription
labelReact.ReactNode-Group heading shown in the toggle button.
childrenReact.ReactNode-Sidebar001Item elements shown when the group is open.
defaultOpen?booleanfalseWhether the group starts expanded.
icon?React.ReactNode-Optional leading icon next to the label. Omit for compact chevron-only style.
className?string-Additional classes.

Sidebar001Item

PropTypeDefaultDescription
hrefstring-Navigation target.
labelReact.ReactNode-Text or node shown as the link label.
isActiveboolean-Marks this item as the current page.
isNew?boolean-Shows a small accent dot after the label.
className?string-Additional classes on the anchor element.
onClick?React.MouseEventHandler<HTMLAnchorElement>-Click handler for the anchor.

Sidebar001Footer

PropTypeDefaultDescription
children?React.ReactNode-Content rendered at the bottom of the sidebar, above a top border.
className?string-Additional classes.

Notes

  • The hover highlight is a single shared motion.div that tracks the hovered item's bounding rect — no per-item background, no re-layout on hover.
  • The active bar uses layoutId="sb001-active-bar" so it animates smoothly between items as the active route changes.
  • useScrollToActive fires once per active item mount via requestIdleCallback, centering the item in the scroll viewport without blocking the initial render.
  • Resize is pointer-capture based — dragging works even if the cursor moves off the handle.

Credits

Built by leo.

Keep in mind

Most components on this site are inspired by or recreated from existing work across the web. I'm not here to take credit; just to learn, experiment, and sometimes push things a bit further. If something looks familiar and I forgot to mention you, reach out and I'll fix that right away.