Pricing Card.

An interactive pricing card with a draggable slider, animated character transitions, and shuffling price values.

Interactive Preview

Scale your business
Up to
1k
Users
Free plan

Installation Guide

** 01. Dependencies **
pnpm add motion lucide-react motion
** 02. Source Code **

Create components/pricing-card.tsx and paste the code below:

"use client";
import React, { useState, useEffect } from "react";
import { motion, AnimatePresence, useSpring, useTransform } from "motion/react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { User, Users, Briefcase, Building, Minus, Plus } from "lucide-react";
const pricingTiers = [
  { maxUsers: 1000, price: 0, icon: User, color: "text-blue-500", bg: "bg-blue-500/15" },
  { maxUsers: 3000, price: 20, icon: Users, color: "text-purple-500", bg: "bg-purple-500/15" },
  { maxUsers: 6000, price: 50, icon: Briefcase, color: "text-orange-500", bg: "bg-orange-500/15" },
  { maxUsers: 9000, price: 90, icon: Building, color: "text-green-500", bg: "bg-green-500/15" },
];
const MAX_VAL = 10000;
export default function PricingCard() {
  const [value, setValue] = useState(1000);
  const isEnterprise = value > 9000;
  const springValue = useSpring(1000, { 
    stiffness: 1000, 
    damping: 100, 
    mass: 0.1 
  });
  const displayValue = useTransform(springValue, (latest) => {
    const val = Math.round(latest);
    if (val >= 1000) {
      return `${parseFloat((val / 1000).toFixed(1))}k`;
    }
    return val.toLocaleString();
  });
  const currentTier = pricingTiers.find(tier => value <= tier.maxUsers) || pricingTiers[pricingTiers.length - 1];
  useEffect(() => {
    if (!isEnterprise) {
      springValue.set(value);
    }
  }, [value, isEnterprise, springValue]);
  const handleSliderChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(parseInt(e.target.value));
  };
  const increment = () => setValue(prev => Math.min(prev + 1000, MAX_VAL));
  const decrement = () => setValue(prev => Math.max(prev - 1000, 1000));
  const calculatePrice = (users: number) => {
    if (users <= 1000) return 0;
    if (users <= 3000) return 20;
    if (users <= 6000) return 50;
    if (users <= 9000) return 90;
    return 0; 
  };
  const price = calculatePrice(value);
  const Icon = isEnterprise ? Building : currentTier.icon;
  return (
    <Card className="w-full max-w-[340px] mx-auto overflow-hidden bg-card border-border text-card-foreground rounded-[2.5rem] transition-colors duration-500 shadow-sm">
      <CardContent className="p-6 flex flex-col items-center gap-6">
        <div className="text-muted-foreground text-[13px] font-bold tracking-tight uppercase opacity-60">
          Scale your business
        </div>
        <div className="relative h-24 w-full flex items-center justify-center">
          <AnimatePresence mode="wait">
            <motion.div
              key={isEnterprise ? 'ent' : currentTier.maxUsers}
              className="relative"
              initial={{ scale: 0.8, opacity: 0, y: 10 }}
              animate={{ scale: 1, opacity: 1, y: 0 }}
              exit={{ scale: 0.8, opacity: 0, y: -10 }}
              transition={{ type: "spring", stiffness: 400, damping: 25 }}
            >
              <div
                className={cn(
                  "p-5 rounded-[2.2rem] transition-colors duration-500 relative z-10 border border-border/50 shadow-sm",
                  isEnterprise ? "bg-muted" : currentTier.bg
                )}
              >
                <Icon className={cn("w-10 h-10 transition-colors duration-500", isEnterprise ? "text-muted-foreground" : currentTier.color)} />
              </div>
            </motion.div>
          </AnimatePresence>
        </div>
        <div className="text-center w-full">
          <div className="flex items-baseline justify-center gap-4 h-12">
            <span className="text-muted-foreground text-sm font-bold uppercase tracking-wider opacity-60">
              {isEnterprise ? "Over" : "Up to"}
            </span>
            <div className="flex justify-center items-baseline">
              <AnimatePresence mode="popLayout" initial={false}>
                {isEnterprise ? (
                  <motion.span
                    key="ent"
                    initial={{ opacity: 0, y: 10 }}
                    animate={{ opacity: 1, y: 0 }}
                    exit={{ opacity: 0, y: -10 }}
                    className="text-5xl font-black tracking-tighter text-foreground leading-none px-1"
                  >
                    10k+
                  </motion.span>
                ) : (
                  <motion.span 
                    key="val"
                    initial={{ opacity: 0, y: 10 }}
                    animate={{ opacity: 1, y: 0 }}
                    exit={{ opacity: 0, y: -10 }}
                    className="text-5xl font-black tracking-tighter text-foreground leading-none px-1"
                  >
                    <motion.span>{displayValue}</motion.span>
                  </motion.span>
                )}
              </AnimatePresence>
            </div>
            <span className="text-muted-foreground text-sm font-bold uppercase tracking-wider opacity-60">
              Users
            </span>
          </div>
        </div>
        <div className="w-full space-y-4">
          <div className="relative h-10 flex items-center px-1">
             <div className="absolute inset-0 flex justify-between items-center pointer-events-none px-2">
                {Array.from({ length: 31 }).map((_, i) => {
                  const normalizedPos = i / 30;
                  const sliderPos = (value - 1000) / (MAX_VAL - 1000);
                  const dist = Math.abs(normalizedPos - sliderPos);
                  const bump = Math.max(0, 1 - dist * 4); 
                  return (
                    <motion.div 
                      key={i} 
                      animate={{ 
                        height: 4 + (bump * 16),
                        backgroundColor: dist < 0.04 ? "var(--foreground)" : "var(--muted-foreground)"
                      }}
                      style={{
                        opacity: dist < 0.04 ? 1 : 0.2
                      }}
                      transition={{ type: "spring", stiffness: 400, damping: 30, mass: 0.5 }}
                      className="w-[2px] rounded-full"
                    />
                  );
                })}
             </div>
             <input
                type="range"
                min="1000"
                max={MAX_VAL}
                step="100"
                value={value}
                onChange={handleSliderChange}
                className="absolute inset-0 w-full h-full opacity-0 cursor-grab active:cursor-grabbing z-10"
             />
          </div>
          <div className="flex justify-center items-center gap-5 pt-1">
            <span className="text-muted-foreground text-[14px] font-bold min-w-[100px] text-center">
              {isEnterprise ? "Enterprise" : (price === 0 ? "Free plan" : `$${price} / month`)}
            </span>
            <div className="flex gap-2">
              <button 
                onClick={decrement}
                className="w-8 h-8 rounded-full bg-muted border border-border flex items-center justify-center hover:bg-accent transition-colors active:scale-90"
              >
                <Minus className="w-3.5 h-3.5 text-muted-foreground" />
              </button>
              <button 
                onClick={increment}
                className="w-8 h-8 rounded-full bg-muted border border-border flex items-center justify-center hover:bg-accent transition-colors active:scale-90"
              >
                <Plus className="w-3.5 h-3.5 text-muted-foreground" />
              </button>
            </div>
          </div>
        </div>
        <Button 
          className={cn(
            "w-full h-12 rounded-2xl text-[15px] font-bold transition-all duration-300 active:scale-[0.98] mt-2",
            isEnterprise 
              ? "bg-foreground text-background hover:opacity-90" 
              : "bg-[#9B87FF] text-white hover:bg-[#8A75FF]"
          )}
        >
          {isEnterprise ? "Get in touch" : (price === 0 ? "Get started" : `Subscribe for $${price}`)}
        </Button>
      </CardContent>
    </Card>
  );
}
** 03. Usage Example **
import PricingCard from "@/components/pricing-card";

export default function MyComponent() {
  return (
    <div className="w-full flex items-center justify-center p-4">
        <PricingCard />
    </div>
  );
}