pricing-1

PreviousNext

Simple pricing block with single plan

Docs
smoothuiui

Preview

Loading preview…
index.tsx
"use client";

import { cn } from "@repo/shadcn-ui/lib/utils";
import { motion } from "motion/react";
import { useEffect, useRef, useState } from "react";

type PriceFlowProps = {
  value: number;
  className?: string;
};

function PriceFlow({ value, className = "" }: PriceFlowProps) {
  const [prevValue, setPrevValue] = useState(value);
  const prevTensRef = useRef<HTMLSpanElement>(null);
  const nextTensRef = useRef<HTMLSpanElement>(null);
  const prevOnesRef = useRef<HTMLSpanElement>(null);
  const nextOnesRef = useRef<HTMLSpanElement>(null);

  useEffect(() => {
    if (value !== prevValue) {
      const prevTens = prevTensRef.current;
      const nextTens = nextTensRef.current;
      const prevOnes = prevOnesRef.current;
      const nextOnes = nextOnesRef.current;

      if (
        prevTens &&
        nextTens &&
        Math.floor(value / 10) !== Math.floor(prevValue / 10)
      ) {
        if (Math.floor(value / 10) > Math.floor(prevValue / 10)) {
          prevTens.classList.add("slide-out-up");
          nextTens.classList.add("slide-in-up");
        } else {
          prevTens.classList.add("slide-out-down");
          nextTens.classList.add("slide-in-down");
        }

        const handleTensAnimationEnd = () => {
          prevTens.classList.remove("slide-out-up", "slide-out-down");
          nextTens.classList.remove("slide-in-up", "slide-in-down");
          prevTens.removeEventListener("animationend", handleTensAnimationEnd);
        };

        prevTens.addEventListener("animationend", handleTensAnimationEnd);
      }

      if (prevOnes && nextOnes && value % 10 !== prevValue % 10) {
        setTimeout(() => {
          if (value % 10 > prevValue % 10) {
            prevOnes.classList.add("slide-out-up");
            nextOnes.classList.add("slide-in-up");
          } else {
            prevOnes.classList.add("slide-out-down");
            nextOnes.classList.add("slide-in-down");
          }

          const handleOnesAnimationEnd = () => {
            prevOnes.classList.remove("slide-out-up", "slide-out-down");
            nextOnes.classList.remove("slide-in-up", "slide-in-down");
            prevOnes.removeEventListener(
              "animationend",
              handleOnesAnimationEnd
            );
          };

          prevOnes.addEventListener("animationend", handleOnesAnimationEnd);
        }, 50);
      }

      setPrevValue(value);
    }
  }, [value, prevValue]);

  const formatValue = (val: number) => val.toString().padStart(2, "0");
  const prevFormatted = formatValue(prevValue);
  const currentFormatted = formatValue(value);

  return (
    <span className={cn("relative inline-flex items-center", className)}>
      <span className="relative inline-block overflow-hidden">
        <span
          className="absolute inset-0 flex items-center justify-center"
          ref={prevTensRef}
          style={{ transform: "translateY(-100%)" }}
        >
          {prevFormatted[0]}
        </span>
        <span
          className="flex items-center justify-center"
          ref={nextTensRef}
          style={{ transform: "translateY(0%)" }}
        >
          {currentFormatted[0]}
        </span>
      </span>
      <span className="relative inline-block overflow-hidden">
        <span
          className="absolute inset-0 flex items-center justify-center"
          ref={prevOnesRef}
          style={{ transform: "translateY(-100%)" }}
        >
          {prevFormatted[1]}
        </span>
        <span
          className="flex items-center justify-center"
          ref={nextOnesRef}
          style={{ transform: "translateY(0%)" }}
        >
          {currentFormatted[1]}
        </span>
      </span>
    </span>
  );
}

export function PricingSimple() {
  const [isAnnual, setIsAnnual] = useState(true);

  return (
    <section>
      <div className="relative bg-muted/50 py-16 md:py-32">
        <div className="mx-auto max-w-5xl px-6">
          <div className="mx-auto max-w-2xl text-center">
            <h2 className="text-balance font-bold text-3xl md:text-4xl lg:text-5xl lg:tracking-tight">
              Simple pricing for everyone
            </h2>
            <p className="mx-auto mt-4 max-w-xl text-balance text-foreground/70 text-lg">
              One plan, all features. No hidden fees, no complicated tiers.
            </p>
            <div className="my-12">
              <div
                className="relative mx-auto grid w-fit grid-cols-2 rounded-full border bg-background p-1 *:block *:h-8 *:w-24 *:rounded-full *:text-foreground *:text-sm *:hover:opacity-75"
                data-period={isAnnual ? "annually" : "monthly"}
              >
                <div
                  aria-hidden="true"
                  className={`pointer-events-none absolute inset-1 w-1/2 rounded-full border border-transparent bg-brand shadow ring-1 ring-foreground/5 transition-transform duration-500 ease-in-out ${
                    isAnnual ? "translate-x-full" : "translate-x-0"
                  }`}
                />
                <button
                  className="relative duration-500 data-[active=true]:font-medium data-[active=true]:text-white"
                  data-active={!isAnnual}
                  onClick={() => setIsAnnual(false)}
                  type="button"
                >
                  Monthly
                </button>
                <button
                  className="relative duration-500 data-[active=true]:font-medium data-[active=true]:text-white"
                  data-active={isAnnual}
                  onClick={() => setIsAnnual(true)}
                  type="button"
                >
                  Annually
                </button>
              </div>
              <div className="mt-3 text-center text-xs">
                <span className="font-medium text-brand">Save 20%</span> On
                Annual Billing
              </div>
            </div>
          </div>
          <div className="container">
            <div className="mx-auto max-w-md">
              <motion.div
                animate={{ opacity: 1, y: 0 }}
                className="group relative flex h-[650px] cursor-pointer flex-col overflow-hidden rounded-2xl border bg-background p-8"
                data-animate-card
                initial={{ opacity: 0, y: 40 }}
                transition={{ duration: 0.3, ease: [0.22, 1, 0.36, 1] }}
              >
                {/* Gradient Accent */}
                <div className="gradient-accent absolute top-0 right-0 h-4 w-32 rounded-bl-2xl bg-gradient-to-r from-green-400 via-blue-400 to-purple-400" />

                <div className="card-content relative z-10 flex h-full flex-col">
                  {/* Title */}
                  <h3 className="mb-4 font-bold text-2xl text-foreground">
                    Pro
                  </h3>
                  {/* Price & Duration */}
                  <div className="mb-6">
                    <span className="font-semibold text-3xl text-foreground">
                      <PriceFlow value={isAnnual ? 15 : 19} />€
                    </span>
                    <span className="mx-2 text-foreground/70">•</span>
                    <span className="text-foreground/70">
                      Perfect for individuals
                    </span>
                  </div>
                  {/* CTA Button */}
                  <button
                    className="mb-6 inline-flex h-10 w-full cursor-pointer items-center justify-center gap-2 rounded-md bg-foreground px-4 py-2 font-medium text-background text-sm transition-colors hover:bg-foreground/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
                    type="button"
                  >
                    Get Started
                  </button>
                  {/* Description */}
                  <p className="mb-6 flex-grow text-foreground/70 text-sm leading-relaxed">
                    Everything you need to build and deploy amazing
                    applications. Simple, powerful, and affordable.
                  </p>
                  {/* What's Included */}
                  <div className="space-y-4">
                    <h4 className="font-medium text-foreground/70 text-xs uppercase tracking-wider">
                      What&apos;s included:
                    </h4>
                    <ul className="space-y-3">
                      {[
                        "Unlimited Projects",
                        "Email Support",
                        "All Features",
                        "Advanced Analytics",
                        "Team Collaboration",
                        "Custom Domains",
                        "Priority Updates",
                        "API Access",
                      ].map((item) => (
                        <li
                          className="flex items-center gap-3 text-foreground text-sm"
                          key={item}
                        >
                          <div className="flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-foreground">
                            <svg
                              aria-hidden="true"
                              className="h-2 w-2 text-background"
                              fill="currentColor"
                              viewBox="0 0 20 20"
                            >
                              <path
                                clipRule="evenodd"
                                d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
                                fillRule="evenodd"
                              />
                            </svg>
                          </div>
                          {item}
                        </li>
                      ))}
                    </ul>
                  </div>
                </div>
              </motion.div>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

export default PricingSimple;

Installation

npx shadcn@latest add @smoothui/pricing-1

Usage

import { Pricing1 } from "@/components/ui/pricing-1"
<Pricing1 />