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.
npx shadcn@latest add https://webinnoventix.com/r/aprice-highlight-follow-plans.json"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")
}
>
£
<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 quoteMore blocks
Simple Pricing
OriginalA three-tier pricing section with a highlighted popular plan.
Single Plan Pricing Card
OriginalA focused one-plan pricing card with price, trial badge, feature list, call to action and a money-back trust note, ideal when you sell a single flat-rate product.
Three Tier Pricing With Monthly Annual Toggle
OriginalA three-tier pricing grid with a pure-CSS monthly and annual billing toggle that swaps every price with no JavaScript, using a native checkbox and Tailwind group-has state.

