Morphing pill
A dynamic pill component with tabs built using React and Framer Motion.
Connect
Installation
Run the following command
It will create a new file pill.tsx
inside the components/cards/pill.tsx
directory.
mkdir -p components/cards && touch components/cards/pill.tsx && components/cards/check-box.tsx
Paste the code
Open the newly created check-box file and paste the following code:
"use client";
import React, { useRef, useState, useEffect } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { Cloud, KeyRound, NotepadText } from "lucide-react";
import { cn } from "@/lib/utils";
type NavState = {
opacity: number;
left: number;
width: number;
};
const Pill = () => {
const [hoverState, setHoverState] = useState<NavState>({
opacity: 0,
left: 0,
width: 0,
});
const [activeState, setActiveState] = useState<NavState>({
opacity: 1,
left: 0,
width: 0,
});
const [active, setActive] = useState<number>(0);
const [isHovering, setIsHovering] = useState(false);
const [isClicked, setIsClicked] = useState(false);
const itemRefs = useRef<(HTMLLIElement | null)[]>([]);
const items = [
{
name: "Connect",
icon: KeyRound,
text: "Info",
description: "248k active users",
color: "#008FFF",
bg: "#001526",
},
{
name: "Contracts",
icon: NotepadText,
text: "Details",
description: "7 active contracts",
color: "#00CB48",
bg: "#001E0B",
},
{
name: "Engine",
icon: Cloud,
text: "Check",
description: "Optimal engine health",
color: "#FF58AE",
bg: "#260D1A",
},
];
useEffect(() => {
if (itemRefs.current[active]) {
const { offsetLeft, offsetWidth } = itemRefs.current[active];
setActiveState({
opacity: 1,
left: offsetLeft,
width: offsetWidth,
});
}
}, [active]);
const handleMouseEnter = (index: number) => {
if (!itemRefs.current[index]) return;
const { offsetLeft, offsetWidth } = itemRefs.current[index];
setHoverState({
opacity: 1,
left: offsetLeft,
width: offsetWidth,
});
setIsHovering(true);
};
const handleMouseLeave = () => {
setHoverState((prev) => ({
...prev,
opacity: 0,
}));
setIsHovering(false);
};
const Icon = items[active].icon;
return (
<div className="w-full h-full flex items-center justify-center dark:bg-black bg-white rounded-xl">
<div className=" w-full h-full flex flex-col items-center justify-center gap-y-10 overflow-hidden">
<motion.div
layout
className="flex cursor-pointer items-center justify-between gap-x-2 overflow-hidden border-[1px] px-2 pl-2.5 h-10 rounded-full"
transition={{
duration: 0.2,
type: "spring",
stiffness: 300,
damping: 25,
}}
animate={{ width: "auto" }}
>
<div className="flex w-auto items-center justify-between gap-2 overflow-hidden">
<div
className="flex items-center justify-center gap-2"
style={{
willChange: "auto",
height: 40,
}}
>
<div className="center size-5">
<Icon
size={20}
style={{
color: items[active].color,
}}
/>
</div>
<motion.span className="w-auto translate-x-0 translate-y-0 select-none font-openrunde text-base font-medium">
{items[active].name}
</motion.span>
</div>
{isClicked && (
<AnimatePresence>
<motion.div
className="flex w-auto items-center justify-center gap-x-2 overflow-hidden"
style={{
opacity: 1,
willChange: "auto",
filter: "none",
}}
>
<span className="select-none font-openrunde text-sm font-medium text-[#999]">
ยท
</span>
<span className="translate-x-0 translate-y-0 select-none text-nowrap font-openrunde text-sm font-medium text-[#999]">
{items[active].description}
</span>
</motion.div>
</AnimatePresence>
)}
</div>
<motion.button
className="rounded-full py-1 px-3 text-sm tracking-tighter "
style={{
color: items[active].color,
backgroundColor: items[active].bg,
}}
onClick={() => setIsClicked(!isClicked)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
transition={{ duration: 0.2 }}
>
{items[active].text}
</motion.button>
</motion.div>
<div className="flex h-8 items-center gap-2 relative justify-center">
{items.map((item, index) => (
<motion.li
ref={(el) => {
itemRefs.current[index] = el;
}}
onMouseEnter={() => handleMouseEnter(index)}
onMouseLeave={handleMouseLeave}
onClick={() => {
setIsClicked(false);
setActive(index);
}}
key={index}
className={cn(
"h-full flex relative items-center justify-center px-3 z-10 cursor-pointer list-none"
)}
whileTap={{ scale: 0.95 }}
transition={{ duration: 0.2 }}
>
{item.name}
</motion.li>
))}
<motion.div
animate={isHovering ? hoverState : activeState}
className="absolute bg-muted rounded z-0 h-full"
transition={{
duration: 0.3,
type: "spring",
stiffness: 300,
damping: 30,
}}
/>
</div>
</div>
</div>
);
};
export default Pill;
Credits
Built by Bossadi Zenith