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;