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

children
React.ReactNode

Sidebar contents — Header, Content, Footer.

className?
string

Additional classes on the root aside element.

defaultEffectsEnabled?
boolean
true

Initial value for the motion effects toggle (persisted in localStorage).

defaultWidth?
number
240

Initial sidebar width in px.

minWidth?
number
160

Minimum drag width in px.

maxWidth?
number
400

Maximum drag width in px.

Sidebar001Header

children?
React.ReactNode

Content rendered at the top of the sidebar.

className?
string

Additional classes.

Sidebar001Content

children
React.ReactNode

Scrollable body — typically Sidebar001Section and Sidebar001Group elements.

className?
string

Additional classes on the scroll container.

Sidebar001Section

label?
React.ReactNode

Section heading shown above the group of items.

children
React.ReactNode

Sidebar001Item elements.

className?
string

Additional classes.

Sidebar001Group

label
React.ReactNode

Group heading shown in the toggle button.

children
React.ReactNode

Sidebar001Item elements shown when the group is open.

defaultOpen?
boolean
false

Whether 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

href
string

Navigation target.

label
React.ReactNode

Text or node shown as the link label.

isActive
boolean

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

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.