Pricing 02Pro
Two plans side by side, each with a feature list and a usage table for included and overage amounts, followed by a full-width Enterprise row with a sales button.
Three pricing tiers built on Pricing Card with a monthly/yearly tab switch, prices that roll digit by digit and an ink-ringed recommended plan.
"use client"
import * as React from "react"
import { AnimatePresence, motion, useReducedMotion } from "motion/react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
PricingCard,
PricingCardContent,
PricingCardCurrency,
PricingCardDescription,
PricingCardFeature,
PricingCardFeatures,
PricingCardFooter,
PricingCardHeader,
PricingCardPeriod,
PricingCardPrice,
PricingCardTitle,
PricingCardValue,
} from "@/components/ui/pricing-card"
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
type Billing = "monthly" | "yearly"
const tiers = [
{
name: "Hobby",
description: "For a personal site or side project.",
price: { monthly: 0, yearly: 0 },
cta: "Start for free",
lead: "Includes",
features: [
"10k pageviews a month",
"2 websites",
"30 days of data",
"Live dashboard and goals",
],
},
{
name: "Growth",
description: "For teams that ship and measure weekly.",
price: { monthly: 19, yearly: 16 },
cta: "Start 14-day trial",
lead: "Everything in Hobby, plus",
features: [
"250k pageviews a month",
"Unlimited websites",
"2 years of data",
"Funnels and revenue goals",
"Weekly reports in Slack",
],
featured: true,
},
{
name: "Business",
description: "For companies running several products.",
price: { monthly: 79, yearly: 66 },
cta: "Start 14-day trial",
lead: "Everything in Growth, plus",
features: [
"2M pageviews a month",
"Unlimited data retention",
"SSO with Google and Okta",
"Roles and shared dashboards",
"Support replies within 4 hours",
],
},
]
/** Rolls only the digits that change, so 19 → 16 moves one column. */
function RollingNumber({ value }: { value: number }) {
const reduceMotion = useReducedMotion()
return (
<span aria-hidden="true" className="inline-flex overflow-hidden tabular-nums">
{String(value)
.split("")
.map((digit, index) => (
<span key={index} className="relative inline-flex overflow-hidden">
<AnimatePresence initial={false} mode="popLayout">
<motion.span
key={digit}
initial={reduceMotion ? false : { y: "70%", opacity: 0 }}
animate={{ y: "0%", opacity: 1 }}
exit={reduceMotion ? undefined : { y: "-70%", opacity: 0 }}
transition={{ type: "spring", bounce: 0.15, duration: 0.4 }}
className="inline-block"
>
{digit}
</motion.span>
</AnimatePresence>
</span>
))}
</span>
)
}
export default function Pricing01() {
const [billing, setBilling] = React.useState<Billing>("yearly")
return (
<section className="bg-background px-4 py-24 sm:px-6 md:py-32">
<div className="mx-auto max-w-6xl">
<div className="mx-auto flex max-w-2xl flex-col items-center gap-4 text-center">
<h2 className="text-4xl font-semibold tracking-tight text-balance">
Pay for pageviews, not for seats.
</h2>
<p className="max-w-xl text-pretty text-muted-foreground">
Every plan includes unlimited team members, a 1 KB cookie-free
script and full data export. Upgrade or downgrade from the
dashboard.
</p>
<Tabs
value={billing}
onValueChange={(value) => setBilling(value as Billing)}
className="mt-4"
>
<TabsList variant="segmented" size="sm" aria-label="Billing period">
<TabsTrigger value="monthly">Monthly</TabsTrigger>
<TabsTrigger value="yearly">
Yearly
<Badge variant="success" size="sm">
−16%
</Badge>
</TabsTrigger>
</TabsList>
</Tabs>
</div>
<div className="mt-12 grid gap-4 md:grid-cols-3">
{tiers.map((tier) => {
const price = tier.price[billing]
const free = tier.price.monthly === 0
return (
<PricingCard key={tier.name} featured={tier.featured}>
<PricingCardHeader>
<PricingCardTitle className="flex items-center justify-between gap-2">
{tier.name}
{tier.featured && (
<Badge variant="brand" stroke>
Most popular
</Badge>
)}
</PricingCardTitle>
<PricingCardDescription>{tier.description}</PricingCardDescription>
</PricingCardHeader>
<div className="flex flex-col gap-1.5">
<PricingCardPrice>
<span className="sr-only">${price}</span>
<PricingCardValue>
<PricingCardCurrency>$</PricingCardCurrency>
<RollingNumber value={price} />
</PricingCardValue>
<PricingCardPeriod>{free ? "forever" : "per month"}</PricingCardPeriod>
</PricingCardPrice>
<p className="h-4 text-xs text-muted-foreground tabular-nums">
{free
? "No card required"
: billing === "yearly"
? `$${tier.price.yearly * 12} billed yearly`
: "Billed monthly"}
</p>
</div>
<PricingCardFooter>
<Button variant={tier.featured ? "default" : "outline"}>
{tier.cta}
</Button>
</PricingCardFooter>
<PricingCardContent className="border-t border-border pt-6">
<p className="text-sm font-medium">{tier.lead}</p>
<PricingCardFeatures>
{tier.features.map((feature) => (
<PricingCardFeature key={feature}>{feature}</PricingCardFeature>
))}
</PricingCardFeatures>
</PricingCardContent>
</PricingCard>
)
})}
</div>
<p className="mt-10 text-center text-sm text-muted-foreground">
Over your limit for a month? We email you first and keep tracking.{" "}
<a
href="#"
className="font-medium text-foreground underline-offset-4 hover:underline"
>
Compare all features
</a>
</p>
</div>
</section>
)
}Writes components/blocks/pricing-01.tsx and installs every component it uses.
npx shadcn@latest add @blank/pricing-01Two plans side by side, each with a feature list and a usage table for included and overage amounts, followed by a full-width Enterprise row with a sales button.
A plan comparison table with a sticky header carrying prices and buttons, grouped feature rows with check and dash marks, a tinted recommended column, and a plan switcher on mobile.