Collapsing sidebar
- Source
- After bencho.dev ↗
- Author
- Interaction after bencho.dev
- Licence
- Original code — interaction after bencho.dev
Loading…
Collapse the sidebar to icons and back; pick a page to move the highlight.
Code
"use client";
import { AnimatePresence, motion } from "motion/react";
import { ChartColumn, Folder, House, Inbox, PanelLeft, Settings } from "lucide-react";
import { useId, useState } from "react";
import { Surface } from "../ui";
const NAV = [
{ label: "Home", Icon: House },
{ label: "Inbox", Icon: Inbox, badge: 3 },
{ label: "Projects", Icon: Folder },
{ label: "Reports", Icon: ChartColumn },
{ label: "Settings", Icon: Settings },
];
export default function CollapsingSidebar() {
const [open, setOpen] = useState(true);
const [active, setActive] = useState(0);
const id = useId();
return (
<Surface className="place-items-stretch p-[1.2em]">
<div className="flex h-full overflow-hidden rounded-[1em] bg-white shadow-[0_12px_32px_-18px_rgba(0,0,0,0.35),0_0_0_1px_rgba(0,0,0,0.04)]">
<motion.aside
animate={{ width: open ? "11em" : "3.6em" }}
transition={{ type: "spring", stiffness: 380, damping: 34 }}
className="flex shrink-0 flex-col gap-[0.25em] border-r border-[#efeee9] bg-[#fbfaf7] p-[0.5em]"
>
<button
type="button"
aria-label="Toggle sidebar"
onClick={() => setOpen((o) => !o)}
className="mb-[0.4em] flex h-[2.4em] items-center gap-[0.6em] rounded-[0.6em] px-[0.65em] text-[#8a8984] hover:bg-[#f1f0ec] hover:text-[#151515]"
>
<PanelLeft className="h-[1.15em] w-[1.15em] shrink-0" />
<AnimatePresence initial={false}>
{open && (
<motion.span initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="whitespace-nowrap text-[0.75em] font-medium">
Collapse
</motion.span>
)}
</AnimatePresence>
</button>
{NAV.map(({ label, Icon, badge }, i) => (
<button
key={label}
type="button"
onClick={() => setActive(i)}
className={`group relative flex h-[2.4em] items-center gap-[0.6em] rounded-[0.6em] px-[0.65em] ${active === i ? "text-[#151515]" : "text-[#6f6d67] hover:text-[#151515]"}`}
>
{active === i && (
<motion.span layoutId={`${id}-a`} className="absolute inset-0 rounded-[0.6em] bg-[#ecebe6]" transition={{ type: "spring", stiffness: 500, damping: 38 }} />
)}
<Icon className="relative h-[1.15em] w-[1.15em] shrink-0" />
<AnimatePresence initial={false}>
{open && (
<motion.span
initial={{ opacity: 0, x: -6, filter: "blur(3px)" }}
animate={{ opacity: 1, x: 0, filter: "blur(0px)", transition: { delay: 0.04 * i } }}
exit={{ opacity: 0, x: -4, transition: { duration: 0.1 } }}
className="relative flex-1 whitespace-nowrap text-left text-[0.82em]"
>
{label}
</motion.span>
)}
</AnimatePresence>
{badge && (
<motion.span
layout
className={`grid place-items-center rounded-full bg-[#2B46B8] font-medium text-white ${open ? "relative h-[1.4em] min-w-[1.4em] px-[0.35em] text-[0.65em]" : "absolute right-[0.5em] top-[0.45em] h-[0.5em] w-[0.5em] text-[0]"}`}
>
{badge}
</motion.span>
)}
{!open && (
<span className="pointer-events-none absolute left-full z-10 ml-[0.6em] hidden whitespace-nowrap rounded-[0.4em] bg-[#151515] px-[0.6em] py-[0.25em] text-[0.7em] text-white group-hover:block">
{label}
</span>
)}
</button>
))}
</motion.aside>
<div className="flex-1 p-[1.2em]">
<AnimatePresence mode="wait">
<motion.div key={active} initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -6 }}>
<p className="font-serif text-[1.5em]">{NAV[active].label}</p>
<div className="mt-[0.8em] grid grid-cols-2 gap-[0.6em]">
{[0, 1, 2, 3].map((k) => (
<div key={k} className="h-[3.6em] rounded-[0.7em] bg-[#f4f3ef]" />
))}
</div>
</motion.div>
</AnimatePresence>
</div>
</div>
</Surface>
);
}
Prompt
Build "Collapsing sidebar", a navigation animation. What it does: Collapse the sidebar to icons and back; pick a page to move the highlight. Write it as a single self-contained React component in TypeScript that fills its parent (w-full h-full), styled with Tailwind CSS, using lucide-react, motion. Technique: an HTML canvas, the Motion library, pointer tracking. It sits on a light background, in a 16:10 frame. Respect prefers-reduced-motion with a still state, pause work when the element is off screen, and keep it accessible. Original: https://bencho.dev (Interaction after bencho.dev, Original code — interaction after bencho.dev).