Tab bar
- Source
- After bencho.dev ↗
- Author
- Interaction after bencho.dev
- Licence
- Original code — interaction after bencho.dev
Loading…
Click a tab and the pill slides over, unfolding its label.
Code
"use client";
import { AnimatePresence, motion } from "motion/react";
import { Bell, Compass, House, Search, User } from "lucide-react";
import { useId, useState } from "react";
import { Surface } from "../ui";
const TABS = [
{ label: "Home", Icon: House, line: "Good morning." },
{ label: "Explore", Icon: Compass, line: "Twelve new places nearby." },
{ label: "Search", Icon: Search, line: "Find anything, fast." },
{ label: "Alerts", Icon: Bell, line: "Two things need you." },
{ label: "Profile", Icon: User, line: "Looking sharp." },
];
export default function TabBar() {
const [active, setActive] = useState(0);
const [hover, setHover] = useState<number | null>(null);
const id = useId();
const T = TABS[active];
return (
<Surface>
<div className="flex flex-col items-center gap-[2em]">
<div className="relative h-[3.2em] w-[20em] overflow-hidden text-center">
<AnimatePresence mode="popLayout" initial={false}>
<motion.div
key={active}
initial={{ opacity: 0, y: 14, filter: "blur(6px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
exit={{ opacity: 0, y: -14, filter: "blur(6px)" }}
transition={{ type: "spring", stiffness: 320, damping: 30 }}
className="absolute inset-0"
>
<p className="font-serif text-[1.6em] leading-tight">{T.label}</p>
<p className="text-[0.75em] text-[#8a8984]">{T.line}</p>
</motion.div>
</AnimatePresence>
</div>
<nav className="flex items-center gap-[0.2em] rounded-full bg-white p-[0.35em] shadow-[0_12px_30px_-16px_rgba(0,0,0,0.35),0_0_0_1px_rgba(0,0,0,0.04)]" onPointerLeave={() => setHover(null)}>
{TABS.map(({ label, Icon }, i) => {
const on = i === active;
return (
<button
key={label}
type="button"
onClick={() => setActive(i)}
onPointerEnter={() => setHover(i)}
className={`relative flex h-[2.6em] items-center gap-[0.4em] rounded-full px-[0.85em] transition-colors ${on ? "text-white" : "text-[#151515]"}`}
>
{hover === i && !on && (
<motion.span layoutId={`${id}-hover`} className="absolute inset-0 rounded-full bg-[#f1f0ec]" transition={{ type: "spring", stiffness: 500, damping: 40 }} />
)}
{on && <motion.span layoutId={`${id}-pill`} className="absolute inset-0 rounded-full bg-[#151515]" transition={{ type: "spring", stiffness: 420, damping: 34 }} />}
<motion.span className="relative" animate={{ scale: on ? 1.05 : 1, rotate: on ? [0, -12, 8, 0] : 0 }} transition={{ duration: 0.4 }}>
<Icon className="h-[1.1em] w-[1.1em]" strokeWidth={2} />
</motion.span>
<AnimatePresence initial={false}>
{on && (
<motion.span
initial={{ width: 0, opacity: 0 }}
animate={{ width: "auto", opacity: 1 }}
exit={{ width: 0, opacity: 0 }}
transition={{ type: "spring", stiffness: 420, damping: 34 }}
className="relative overflow-hidden whitespace-nowrap text-[0.8em] font-medium"
>
{label}
</motion.span>
)}
</AnimatePresence>
</button>
);
})}
</nav>
</div>
</Surface>
);
}
Prompt
Build "Tab bar", a navigation animation. What it does: Click a tab and the pill slides over, unfolding its label. 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).