Baktiyar Ablaikhan

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