Docs
Pricing
Pricing component that display the pricing options of various plans in an sleek and interactive way
Installation
Install dependencies
npm install framer-motion lucide-react
Run the following command
It will create a new file pricing.tsx
inside the components/animata/section
directory.
mkdir -p components/animata/section && touch components/animata/section/pricing.tsx
Paste the code
Open the newly created file and paste the following code:
import React, { useEffect, useRef, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { cn } from "@/lib/utils";
interface Plan {
name: string;
monthlyPrice: string;
yearlyPrice: string;
popular?: boolean;
}
interface PricingProps {
plans: Plan[];
onPlanSelect?: (plan: string) => void;
onCycleChange?: (cycle: "Monthly" | "Yearly") => void;
width?: "sm" | "md" | "lg" | "xl";
outerRadius?: "normal" | "rounded" | "moreRounded";
padding?: "small" | "medium" | "large";
}
const widthClasses = {
sm: "w-full sm:w-[300px]",
md: "w-full sm:w-[300px] md:w-[500px]",
lg: "w-full sm:w-[300px] md:w-[500px] lg:w-[768px]",
xl: "w-full sm:w-[300px] md:w-[500px] lg:w-[768px] xl:w-[1024px]",
};
const outerRadiusClasses = {
normal: "rounded-[16px]",
rounded: "rounded-[24px]",
moreRounded: "rounded-[32px]",
};
const paddingClasses = {
small: "p-2",
medium: "p-3",
large: "p-4",
};
const innerRadiusClasses = {
normal: "rounded-xl",
rounded: "rounded-2xl",
moreRounded: "rounded-3xl",
};
export default function Pricing({
plans,
width = "lg",
outerRadius = "rounded",
padding = "medium",
}: PricingProps) {
const [selectedPlan, setSelectedPlan] = useState("Basic");
const [billingCycle, setBillingCycle] = useState<"Monthly" | "Yearly">("Monthly");
const handlePlanSelect = (planName: string) => {
setSelectedPlan(planName);
};
const handleCycleChange = (cycle: "Monthly" | "Yearly") => {
setBillingCycle(cycle);
};
return (
<div
className={cn(
"mx-auto bg-white shadow-lg",
widthClasses[width],
outerRadiusClasses[outerRadius],
paddingClasses[padding],
)}
>
<div className="mb-3 flex justify-center">
<div className="relative w-3/4 rounded-full bg-zinc-300 p-1 pb-2">
<motion.div
className="absolute h-[38px] w-[calc(50%-6px)] rounded-full bg-zinc-100"
layoutId="cycleBackground"
initial={billingCycle === "Monthly" ? { x: 2 } : { x: "calc(100% + 2px)" }}
animate={billingCycle === "Monthly" ? { x: 2 } : { x: "calc(100% + 2px)" }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
/>
<div className="relative z-10 flex">
{["Monthly", "Yearly"].map((cycle) => (
<motion.button
key={cycle}
className={cn(
"z-20 w-1/2 rounded-full py-1 text-lg font-extrabold transition-colors duration-200",
billingCycle === cycle ? "text-zinc-800" : "text-zinc-500",
)}
onClick={(e) => {
e.stopPropagation();
handleCycleChange(cycle as "Monthly" | "Yearly");
}}
whileTap={{ scale: 0.95 }}
>
{cycle}
</motion.button>
))}
</div>
</div>
</div>
{plans.map((plan) => (
<motion.div
key={plan.name}
className={cn(
"relative mb-3 cursor-pointer border-2 border-zinc-200 p-4",
innerRadiusClasses[outerRadius],
selectedPlan === plan.name ? "bg-zinc-100" : "bg-white",
)}
onClick={() => handlePlanSelect(plan.name)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
layout
>
<AnimatePresence>
{selectedPlan === plan.name && (
<motion.div
className={cn(
"absolute inset-0 border-4 border-zinc-900",
innerRadiusClasses[outerRadius],
)}
layoutId="selectedPlanBorder"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
/>
)}
</AnimatePresence>
<div className="relative z-10 flex items-center justify-between">
<div>
<span className="font-bold">{plan.name}</span>
{plan.popular && (
<span className="ml-2 rounded bg-yellow-300 px-2 py-1 text-xs">Popular</span>
)}
</div>
<motion.div
className={cn(
"flex h-6 w-6 items-center justify-center rounded-full border-2",
selectedPlan === plan.name ? "border-zinc-900 bg-zinc-900" : "border-zinc-300",
)}
animate={{ scale: selectedPlan === plan.name ? 1 : 0.8 }}
>
{selectedPlan === plan.name && (
<motion.div
className="h-3 w-3 rounded-full bg-white"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
/>
)}
</motion.div>
</div>
<div className="relative z-10 mt-2">
<AnimatedPrice
monthlyPrice={plan.monthlyPrice}
yearlyPrice={plan.yearlyPrice}
billingCycle={billingCycle}
/>
</div>
</motion.div>
))}
<motion.button
className={cn("w-full bg-black py-3 font-bold text-white", innerRadiusClasses[outerRadius])}
whileTap={{ scale: 0.95 }}
>
Get Started
</motion.button>
</div>
);
}
interface AnimatedPriceProps {
monthlyPrice: string;
yearlyPrice: string;
billingCycle: "Monthly" | "Yearly";
}
function AnimatedPrice({
monthlyPrice,
yearlyPrice,
billingCycle,
}: AnimatedPriceProps): React.JSX.Element {
const [price, setPrice] = useState(monthlyPrice);
const animationRef = useRef<number | null>(null);
useEffect(() => {
const targetPrice = billingCycle === "Monthly" ? monthlyPrice : yearlyPrice;
const startValue = parseFloat(price.replace(/[^0-9.-]+/g, ""));
const endValue = parseFloat(targetPrice.replace(/[^0-9.-]+/g, ""));
const duration = 50; // Animation duration in milliseconds
const startTime = Date.now();
const animatePrice = () => {
const elapsedTime = Date.now() - startTime;
const progress = Math.min(elapsedTime / duration, 1);
const currentValue = startValue + (endValue - startValue) * progress;
setPrice(`$${currentValue.toFixed(2)}`);
if (progress < 1) {
animationRef.current = requestAnimationFrame(animatePrice);
} else {
setPrice(targetPrice);
}
};
animatePrice();
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [price, billingCycle, monthlyPrice, yearlyPrice]);
return (
<div>
<span className="text-2xl font-bold">{price}</span>
<span className="text-zinc-500">/{billingCycle.toLowerCase().slice(0, -2)}</span>
</div>
);
}
Credits
Built by SatyamVyas04 Inspired by Nitish Khagwal