import { Bookmark, BookmarkCheck } from "lucide-react";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
export default function App() {
const [isBookmarked, setIsBookmarked] = useState(false);
function toggleBookmark() {
setIsBookmarked((prev) => !prev);
}
return (
<div className="flex items-center justify-center h-screen">
<motion.button
className="flex items-center justify-center py-2 px-4 w-fit border border-black/50 outline-none rounded-full bg-black cursor-pointer overflow-hidden"
onClick={toggleBookmark}
whileTap={{ scale: 0.95 }}
whileHover={{ scale: 1.05 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
<AnimatePresence mode="wait" initial={false}>
{isBookmarked ? (
<motion.div
key="bookmarked"
className="flex items-center"
initial={{ opacity: 0, y: 10, scale: 0.8 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -10, scale: 0.8 }}
transition={{ duration: 0.2, ease: "easeOut" }}
>
<motion.div
initial={{ rotate: -20, scale: 0.5 }}
animate={{ rotate: 0, scale: 1 }}
transition={{
type: "spring",
stiffness: 500,
damping: 15,
delay: 0.1,
}}
>
<BookmarkCheck className="w-5 h-5 text-white" />
</motion.div>
<span className="mx-2 text-lg text-white">|</span>
<span className="text-sm text-white">Bookmarked</span>
</motion.div>
) : (
<motion.div
key="bookmark"
className="flex items-center"
initial={{ opacity: 0, y: 10, scale: 0.8 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -10, scale: 0.8 }}
transition={{ duration: 0.2, ease: "easeOut" }}
>
<motion.div
initial={{ rotate: 20, scale: 0.5 }}
animate={{ rotate: 0, scale: 1 }}
transition={{
type: "spring",
stiffness: 500,
damping: 15,
delay: 0.1,
}}
>
<Bookmark className="w-5 h-5 text-white" />
</motion.div>
<span className="mx-2 text-lg text-white">|</span>
<span className="text-sm text-white">Bookmark</span>
</motion.div>
)}
</AnimatePresence>
</motion.button>
</div>
);
}Component details
Overview, install command, dependencies, and project links.
An animated bookmark toggle button built with React, Tailwind CSS, and Framer Motion. Click it once - the icon swaps from Bookmark to BookmarkCheck with a satisfying spring animation. Click again - it snaps back. The icon entrance uses a combination of opacity, vertical slide, scale, and a subtle rotation spring so it never feels like a flat toggle. AnimatePresence with mode="wait" handles the exit animation before the new icon enters, so both never appear on screen at the same time. The button itself scales up slightly on hover and compresses on tap using whileHover and whileTap - all physics-based with spring easing. Icons are from Lucide React. Single component, zero prop drilling, state lives in one useState hook.
Discussion
Feedback, implementation tips, and usage notes.
Comments
Sign in to join the conversation
Similar Components
More from the same category

