AIEO-ready websites that get cited by AITalk to us

Highlight Follow Pricing Plans

A pricing row where an animated gradient highlight glides via shared layout animation to whichever plan you hover, a three-way billing switch slides its pill, prices recount instantly and a perks marquee scrolls beneath.

React + Tailwindnpm: motionanimated highlightframer motion pricinglayout animationbilling togglepricing marquee
Original · free to use
npx shadcn@latest add https://webinnoventix.com/r/aprice-highlight-follow-plans.json
aprice-highlight-follow-plans.tsx
"use client";

import { useEffect, useState } from "react";
import { motion, useSpring, useTransform, useReducedMotion } from "motion/react";

type Cycle = "monthly" | "quarterly" | "annual";

type Plan = {
  name: string;
  blurb: string;
  price: Record<Cycle, number>;
  cta: string;
  features: string[];
};

const plans: Plan[] = [
  {
    name: "Basic",
    blurb: "The essentials to get moving.",
    price: { monthly: 15, quarterly: 13, annual: 11 },
    cta: "Choose Basic",
    features: ["3 seats", "20 GB storage", "Email support", "Core integrations"],
  },
  {
    name: "Pro",
    blurb: "For teams that need more room to run.",
    price: { monthly: 42, quarterly: 37, annual: 32 },
    cta: "Choose Pro",
    features: [
      "15 seats",
      "1 TB storage",
      "Priority support",
      "All integrations",
      "Advanced reporting",
    ],
  },
  {
    name: "Premium",
    blurb: "Full power for demanding workloads.",
    price: { monthly: 78, quarterly: 69, annual: 59 },
    cta: "Choose Premium",
    features: ["Unlimited seats", "Unlimited storage", "24/7 support", "Custom SLA", "Audit logs"],
  },
];

const cycles: { key: Cycle; label: string }[] = [
  { key: "monthly", label: "Monthly" },
  { key: "quarterly", label: "Quarterly" },
  { key: "annual", label: "Annual" },
];

const perks = [
  "No setup fees",
  "Cancel anytime",
  "30 day money back",
  "GDPR compliant",
  "99.9% uptime",
  "Free migration",
  "Unlimited exports",
  "Encrypted at rest",
];

const container = {
  hidden: {},
  show: { transition: { staggerChildren: 0.13, delayChildren: 0.05 } },
};

const item = {
  hidden: { opacity: 0, y: 36 },
  show: {
    opacity: 1,
    y: 0,
    transition: { type: "spring" as const, stiffness: 88, damping: 15 },
  },
};

function AnimatedPrice({ value, reduce }: { value: number; reduce: boolean }) {
  const spring = useSpring(value, { stiffness: 160, damping: 26, mass: 0.6 });
  const text = useTransform(spring, (v) => Math.round(v).toString());
  useEffect(() => {
    spring.set(value);
  }, [value, spring]);
  if (reduce) return <span>{value}</span>;
  return <motion.span>{text}</motion.span>;
}

export default function HighlightFollowPricing() {
  const defaultActive = 1;
  const [cycle, setCycle] = useState<Cycle>("annual");
  const [active, setActive] = useState(defaultActive);
  const reduce = useReducedMotion() ?? false;

  return (
    <section className="relative overflow-hidden bg-white px-6 py-20 dark:bg-zinc-950 md:py-28">
      <style>{`
        @keyframes aprice-ticker { from { transform: translateX(0); } to { transform: translateX(-50%); } }
        .aprice-ticker-track { display: flex; width: max-content; animation: aprice-ticker 26s linear infinite; }
        .aprice-ticker:hover .aprice-ticker-track { animation-play-state: paused; }
        @media (prefers-reduced-motion: reduce) {
          .aprice-ticker-track { animation: none; }
        }
      `}</style>

      <div className="relative mx-auto max-w-6xl">
        <motion.div
          initial={{ opacity: 0, y: 16 }}
          whileInView={{ opacity: 1, y: 0 }}
          viewport={{ once: true, amount: 0.6 }}
          transition={{ duration: 0.5 }}
          className="text-center"
        >
          <h2 className="text-3xl font-bold tracking-tight text-balance text-zinc-900 sm:text-4xl dark:text-white">
            Find the plan that fits
          </h2>
          <p className="mx-auto mt-3 max-w-lg text-zinc-600 dark:text-zinc-400">
            Hover a plan to bring it into focus. Longer billing cycles unlock a better rate,
            recalculated instantly.
          </p>
        </motion.div>

        <div className="mt-8 flex justify-center">
          <div
            role="tablist"
            aria-label="Billing cycle"
            className="relative inline-flex rounded-full border border-zinc-200 bg-zinc-100 p-1 dark:border-zinc-800 dark:bg-zinc-900"
          >
            {cycles.map((c) => {
              const isActive = cycle === c.key;
              return (
                <button
                  key={c.key}
                  role="tab"
                  aria-selected={isActive}
                  onClick={() => setCycle(c.key)}
                  className="relative z-10 rounded-full px-4 py-2 text-sm font-semibold transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 sm:px-6"
                >
                  {isActive && (
                    <motion.span
                      layoutId="aprice-cycle-pill"
                      className="absolute inset-0 -z-10 rounded-full bg-white shadow-sm dark:bg-zinc-700"
                      transition={{ type: "spring", stiffness: 420, damping: 34 }}
                    />
                  )}
                  <span
                    className={
                      isActive
                        ? "text-zinc-900 dark:text-white"
                        : "text-zinc-500 dark:text-zinc-400"
                    }
                  >
                    {c.label}
                  </span>
                </button>
              );
            })}
          </div>
        </div>

        <motion.div
          variants={container}
          initial="hidden"
          whileInView="show"
          viewport={{ once: true, amount: 0.2 }}
          onMouseLeave={() => setActive(defaultActive)}
          className="mt-12 grid gap-5 lg:grid-cols-3"
        >
          {plans.map((plan, index) => {
            const isActive = active === index;
            return (
              <motion.div
                key={plan.name}
                variants={item}
                onMouseEnter={() => setActive(index)}
                onFocusCapture={() => setActive(index)}
                className="relative flex h-full flex-col rounded-3xl p-8"
              >
                {isActive && (
                  <motion.div
                    layoutId="aprice-active-highlight"
                    aria-hidden="true"
                    className="absolute inset-0 -z-10 rounded-3xl bg-gradient-to-b from-indigo-500 to-violet-600 shadow-2xl shadow-indigo-500/30"
                    transition={{ type: "spring", stiffness: 320, damping: 30 }}
                  />
                )}
                <div
                  className={
                    "absolute inset-0 -z-20 rounded-3xl border " +
                    (isActive
                      ? "border-transparent"
                      : "border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900/60")
                  }
                />

                <div className="flex items-center justify-between">
                  <h3
                    className={
                      "text-lg font-semibold transition-colors " +
                      (isActive ? "text-white" : "text-zinc-900 dark:text-white")
                    }
                  >
                    {plan.name}
                  </h3>
                  {index === defaultActive && (
                    <span
                      className={
                        "rounded-full px-2.5 py-1 text-xs font-semibold transition-colors " +
                        (isActive
                          ? "bg-white/20 text-white"
                          : "bg-indigo-500/10 text-indigo-600 dark:text-indigo-300")
                      }
                    >
                      Recommended
                    </span>
                  )}
                </div>

                <p
                  className={
                    "mt-1 text-sm transition-colors " +
                    (isActive ? "text-indigo-100" : "text-zinc-500 dark:text-zinc-400")
                  }
                >
                  {plan.blurb}
                </p>

                <div className="mt-6 flex items-baseline gap-1">
                  <span
                    className={
                      "text-5xl font-bold tracking-tight tabular-nums transition-colors " +
                      (isActive ? "text-white" : "text-zinc-900 dark:text-white")
                    }
                  >
                    &pound;
                    <AnimatedPrice value={plan.price[cycle]} reduce={reduce} />
                  </span>
                  <span
                    className={
                      "text-sm transition-colors " +
                      (isActive ? "text-indigo-100" : "text-zinc-500 dark:text-zinc-400")
                    }
                  >
                    /mo
                  </span>
                </div>
                <p
                  className={
                    "mt-1 text-xs transition-colors " +
                    (isActive ? "text-indigo-200" : "text-zinc-500 dark:text-zinc-400")
                  }
                >
                  Billed {cycle}
                </p>

                <a
                  href="#"
                  className={
                    "mt-6 inline-flex items-center justify-center rounded-xl px-4 py-3 text-sm font-semibold transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 " +
                    (isActive
                      ? "bg-white text-indigo-700 hover:bg-indigo-50"
                      : "border border-zinc-200 bg-white text-zinc-900 hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-white dark:hover:bg-zinc-800")
                  }
                >
                  {plan.cta}
                </a>

                <ul
                  className={
                    "mt-8 space-y-3 border-t pt-6 text-sm transition-colors " +
                    (isActive ? "border-white/20" : "border-zinc-100 dark:border-zinc-800")
                  }
                >
                  {plan.features.map((feature) => (
                    <li
                      key={feature}
                      className={
                        "flex items-start gap-3 transition-colors " +
                        (isActive ? "text-indigo-50" : "text-zinc-700 dark:text-zinc-300")
                      }
                    >
                      <svg
                        className={
                          "mt-0.5 h-5 w-5 shrink-0 transition-colors " +
                          (isActive ? "text-white" : "text-indigo-600 dark:text-indigo-400")
                        }
                        viewBox="0 0 20 20"
                        fill="currentColor"
                        aria-hidden="true"
                      >
                        <path
                          fillRule="evenodd"
                          d="M16.7 5.3a1 1 0 0 1 0 1.4l-7.5 7.5a1 1 0 0 1-1.4 0L3.3 10.7a1 1 0 1 1 1.4-1.4l3.8 3.8 6.8-6.8a1 1 0 0 1 1.4 0Z"
                          clipRule="evenodd"
                        />
                      </svg>
                      <span>{feature}</span>
                    </li>
                  ))}
                </ul>
              </motion.div>
            );
          })}
        </motion.div>

        <div
          className="aprice-ticker relative mt-12 overflow-hidden [mask-image:linear-gradient(to_right,transparent,black_10%,black_90%,transparent)]"
          aria-label="Included with every plan"
        >
          <div className="aprice-ticker-track gap-3">
            {[...perks, ...perks].map((perk, i) => (
              <span
                key={`perk-${i}`}
                aria-hidden={i >= perks.length ? "true" : undefined}
                className="inline-flex shrink-0 items-center gap-2 rounded-full border border-zinc-200 bg-zinc-50 px-4 py-2 text-sm font-medium text-zinc-700 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-300"
              >
                <svg
                  className="h-4 w-4 text-emerald-500"
                  viewBox="0 0 20 20"
                  fill="currentColor"
                  aria-hidden="true"
                >
                  <path
                    fillRule="evenodd"
                    d="M16.7 5.3a1 1 0 0 1 0 1.4l-7.5 7.5a1 1 0 0 1-1.4 0L3.3 10.7a1 1 0 1 1 1.4-1.4l3.8 3.8 6.8-6.8a1 1 0 0 1 1.4 0Z"
                    clipRule="evenodd"
                  />
                </svg>
                {perk}
              </span>
            ))}
          </div>
        </div>
      </div>
    </section>
  );
}

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

More blocks

All components
Chat with us