l8components

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/pricing

Usage

import { PricingBlock } from '@/components/blocks/pricing';

export default function Page() {
  return <PricingBlock />;
}

Source

components/blocks/pricing.tsx
"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>
  )
}

On this page