Baktiyar Ablaikhan

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