Sidebar 001
Animated vertical sidebar with spring hover highlight, animated active bar, collapsible groups, and drag-to-resize.
Installation
File Structure
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:
| Component | Role |
|---|---|
Sidebar001 | Root — provides context, sets width, renders resize handle |
Sidebar001Header | Fixed top slot |
Sidebar001Content | Scrollable body — place sections and groups here |
Sidebar001Section | Flat list of items with an optional heading |
Sidebar001Group | Collapsible group with animated open/close |
Sidebar001Item | Individual navigation link |
Sidebar001Footer | Fixed 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
| Prop | Type | Default | Description |
|---|---|---|---|
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
| Prop | Type | Default | Description |
|---|---|---|---|
children? | React.ReactNode | - | Content rendered at the top of the sidebar. |
className? | string | - | Additional classes. |
Sidebar001Content
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | - | Scrollable body — typically Sidebar001Section and Sidebar001Group elements. |
className? | string | - | Additional classes on the scroll container. |
Sidebar001Section
| Prop | Type | Default | Description |
|---|---|---|---|
label? | React.ReactNode | - | Section heading shown above the group of items. |
children | React.ReactNode | - | Sidebar001Item elements. |
className? | string | - | Additional classes. |
Sidebar001Group
| Prop | Type | Default | Description |
|---|---|---|---|
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
| Prop | Type | Default | Description |
|---|---|---|---|
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
| Prop | Type | Default | Description |
|---|---|---|---|
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.divthat 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. useScrollToActivefires once per active item mount viarequestIdleCallback, 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.