Pricing
Three-tier pricing with a monthly/yearly switch and rolling price animation.
Simple, transparent pricing
-20%
Starter
For side projects and experiments.
€0/ month
- 3 projects
- Community support
- Basic analytics
Most popular
Team
For growing teams shipping every week.
€29/ month
- Unlimited projects
- Priority support
- Advanced analytics
- SSO
Enterprise
For organisations with custom needs.
€99/ month
- Everything in Team
- Audit logs
- Dedicated manager
Installation
npx shadcn@latest add @l8/pricingUsage
import { PricingBlock } from '@/components/blocks/pricing';
export default function Page() {
return <PricingBlock />;
}Source
"use client"
import * as React from "react"
import { AnimatePresence, motion } from "motion/react"
import { CheckIcon } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import { cn } from "@/lib/utils"
const plans = [
{ name: "Starter", monthly: 0, text: "For side projects and experiments.", features: ["3 projects", "Community support", "Basic analytics"] },
{
name: "Team",
monthly: 29,
text: "For growing teams shipping every week.",
features: ["Unlimited projects", "Priority support", "Advanced analytics", "SSO"],
featured: true,
},
{ name: "Enterprise", monthly: 99, text: "For organisations with custom needs.", features: ["Everything in Team", "Audit logs", "Dedicated manager"] },
]
export function PricingBlock() {
const [yearly, setYearly] = React.useState(false)
return (
<section className="@container px-6 py-16">
<div className="mx-auto max-w-2xl text-center">
<h2 className="text-3xl font-semibold tracking-tight">Simple, transparent pricing</h2>
<div className="mt-6 inline-flex items-center gap-3 rounded-full border bg-muted/50 px-4 py-2">
<Label htmlFor="billing">Monthly</Label>
<Switch id="billing" checked={yearly} onCheckedChange={setYearly} />
<Label htmlFor="billing">Yearly</Label>
<Badge variant="secondary" className="rounded-full">-20%</Badge>
</div>
</div>
<div className="mx-auto mt-10 grid max-w-5xl items-stretch gap-4 @xl:grid-cols-3">
{plans.map((plan, i) => {
const price = yearly ? Math.round(plan.monthly * 0.8) : plan.monthly
return (
<motion.div
key={plan.name}
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: i * 0.1 }}
className={cn(
"relative flex flex-col rounded-xl border bg-card p-6",
plan.featured && "border-transparent shadow-xl shadow-primary/10 ring-2 ring-primary"
)}
>
{plan.featured && (
<Badge className="absolute -top-3 left-6 rounded-full">Most popular</Badge>
)}
<h3 className="font-medium">{plan.name}</h3>
<p className="mt-1 text-sm text-muted-foreground">{plan.text}</p>
<div className="mt-6 flex items-baseline gap-1">
<span className="text-4xl font-semibold">€</span>
<span className="relative inline-flex h-10 overflow-hidden text-4xl font-semibold tabular-nums">
<AnimatePresence mode="popLayout" initial={false}>
<motion.span
key={price}
initial={{ y: "100%", opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: "-100%", opacity: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 25 }}
>
{price}
</motion.span>
</AnimatePresence>
</span>
<span className="text-sm text-muted-foreground">/ month</span>
</div>
<ul className="mt-6 flex flex-1 flex-col gap-2 text-sm">
{plan.features.map((f) => (
<li key={f} className="flex items-center gap-2">
<CheckIcon className="size-4 text-primary" />
{f}
</li>
))}
</ul>
<Button className="mt-8 w-full rounded-full" variant={plan.featured ? "default" : "outline"} size="lg">
{plan.monthly === 0 ? "Start for free" : "Get started"}
</Button>
</motion.div>
)
})}
</div>
</section>
)
}