Star Button
A toggle-able star button with animated icon and counter effects, perfect for favoriting or starring content.
Preview
Installation
Install dependencies
# Install Shadcn Button component
npx shadcn@latest add button
# Install Framer Motion
npm install framer-motion
# Install Lucide React for icons
npm install lucide-react
Create the component file
mkdir -p components/shsfui/button && touch components/shsfui/button/star-button.tsx
Add the component code
Open the newly created file and paste the following code:
"use client";
import * as React from "react";
import { StarIcon } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import { Button, type ButtonProps } from "@/components/ui/button";
import { cn } from "@/utils/cn";
type StarButtonProps = ButtonProps & {
initialCount?: number;
initialStarred?: boolean;
label?: string;
onStarChange?: (isStarred: boolean, count: number) => void;
};
const StarButton = React.forwardRef<HTMLButtonElement, StarButtonProps>(
(props, ref) => {
const {
className,
initialCount = 411,
initialStarred = false,
label = "Star",
onStarChange,
...restProps
} = props;
const [starred, setStarred] = React.useState(initialStarred);
const [count, setCount] = React.useState(initialCount);
const starVariants = {
initial: { scale: 1, rotate: 0 },
starred: {
scale: [1, 1.3, 1],
rotate: [0, 15, 0],
transition: { duration: 0.4 },
},
tap: { scale: 0.9 },
};
const countVariants = {
initial: { y: -15, opacity: 0 },
animate: { y: 0, opacity: 1 },
exit: { y: 15, opacity: 0 },
};
const handleStar = React.useCallback(() => {
const newStarred = !starred;
const newCount = count + (newStarred ? 1 : -1);
setStarred(newStarred);
setCount(newCount);
if (onStarChange) {
onStarChange(newStarred, newCount);
}
}, [starred, count, onStarChange]);
return (
<Button
ref={ref}
onClick={handleStar}
className={cn("", className)}
type="button"
{...restProps}
>
<motion.div
variants={starVariants}
initial="initial"
animate={starred ? "starred" : "initial"}
whileTap="tap"
>
<StarIcon
className={cn(
"-ms-1 transition-colors duration-300",
starred ? "text-yellow-400 fill-yellow-400" : "opacity-60"
)}
size={16}
aria-hidden="true"
/>
</motion.div>
<span className="flex items-baseline gap-2">
{label}
<AnimatePresence mode="wait">
<motion.span
key={count}
variants={countVariants}
initial="initial"
animate="animate"
exit="exit"
transition={{ duration: 0.2 }}
className="text-primary-foreground/60 text-xs"
>
{count}
</motion.span>
</AnimatePresence>
</span>
</Button>
);
}
);
StarButton.displayName = "StarButton";
export StarButton;