Pricing · Free prompt
Pricing Table
Pricing Table is a complete, paste-ready pricing build-prompt — a full art-direction spec for Lovable, Bolt, v0, Cursor and Claude. It's free: copy it below and paste it into your builder.

Pricing TablePricing · free prompt · copy-paste
A feature comparison pricing table component with animated pricing, interval switching, and plan selection.
Features
- Animated price transitions with NumberFlow
- Monthly/Yearly interval toggle
- Interactive plan selection
- Feature comparison matrix
- Responsive design
- Dark mode support
- Popular plan highlighting
- Customizable styling
— Install —
npx shadcn@latest add "https://21st.dev/r/pricing-table"
— Component source (Pricing Table.tsx) —
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import { CheckIcon, ArrowRightIcon } from "@radix-ui/react-icons"
import NumberFlow from "@number-flow/react"
export type PlanLevel = "starter" | "pro" | "all" | string
export interface PricingFeature {
name: string
included: PlanLevel | null
}
export interface PricingPlan {
name: string
level: PlanLevel
price: {
monthly: number
yearly: number
}
popular?: boolean
}
export interface PricingTableProps
extends React.HTMLAttributes<HTMLDivElement> {
features: PricingFeature[]
plans: PricingPlan[]
onPlanSelect?: (plan: PlanLevel) => void
defaultPlan?: PlanLevel
defaultInterval?: "monthly" | "yearly"
containerClassName?: string
buttonClassName?: string
}
export function PricingTable({
features,
plans,
onPlanSelect,
defaultPlan = "pro",
defaultInterval = "monthly",
className,
containerClassName,
buttonClassName,
...props
}: PricingTableProps) {
const [isYearly, setIsYearly] = React.useState(defaultInterval === "yearly")
const [selectedPlan, setSelectedPlan] = React.useState<PlanLevel>(defaultPlan)
const handlePlanSelect = (plan: PlanLevel) => {
setSelectedPlan(plan)
onPlanSelect?.(plan)
}
return (
<section
className={cn(
"bg-background text-foreground",
"py-12 sm:py-24 md:py-32 px-4",
"fade-bottom overflow-hidden pb-0",
)}
>
<div
className={cn("w-full max-w-3xl mx-auto px-4", containerClassName)}
{...props}
>
<div className="flex justify-end mb-4 sm:mb-8">
<div className="inline-flex items-center gap-2 text-xs sm:text-sm">
<button
type="button"
onClick={() => setIsYearly(false)}
className={cn(
"px-3 py-1 rounded-md transition-colors",
!isYearly ? "bg-zinc-100 dark:bg-zinc-800" : "text-zinc-500",
)}
>
Monthly
</button>
<button
type="button"
onClick={() => setIsYearly(true)}
className={cn(
"px-3 py-1 rounded-md transition-colors",
isYearly ? "bg-zinc-100 dark:bg-zinc-800" : "text-zinc-500",
)}
>
Yearly
</button>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-4 mb-8">
{plans.map((plan) => (
<button
key={plan.name}
type="button"
onClick={() => handlePlanSelect(plan.level)}
className={cn(
"flex-1 p-4 rounded-xl text-left transition-all",
"border border-zinc-200 dark:border-zinc-800",
selectedPlan === plan.level &&
"ring-2 ring-blue-500 dark:ring-blue-400",
)}
>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium">{plan.name}</span>
{plan.popular && (
<span className="text-xs bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300 px-2 py-0.5 rounded-full">
Popular
</span>
)}
</div>
<div className="flex items-baseline gap-1">
<NumberFlow
format={{
style: "currency",
currency: "USD",
trailingZeroDisplay: "stripIfInteger",
}}
value={isYearly ? plan.price.yearly : plan.price.monthly}
className="text-2xl font-bold"
/>
<span className="text-sm font-normal text-zinc-500">
/{isYearly ? "year" : "month"}
</span>
</div>
</button>
))}
</div>
<div className="border border-zinc-200 dark:border-zinc-800 rounded-xl overflow-hidden">
<div className="overflow-x-auto">
<div className="min-w-[640px] divide-y divide-zinc-200 dark:divide-zinc-800">
<div className="flex items-center p-4 bg-zinc-50 dark:bg-zinc-900">
<div className="flex-1 text-sm font-medium">Features</div>
<div className="flex items-center gap-8 text-sm">
{plans.map((plan) => (
<div
key={plan.level}
className="w-16 text-center font-medium"
>
{plan.name}
</div>
))}
</div>
</div>
{features.map((feature) => (
<div
key={feature.name}
className={cn(
"flex items-center p-4 transition-colors",
feature.included === selectedPlan &&
"bg-blue-50/50 dark:bg-blue-900/20",
)}
>
<div className="flex-1 text-sm">{feature.name}</div>
<div className="flex items-center gap-8 text-sm">
{plans.map((plan) => (
<div
key={plan.level}
className={cn(
"w-16 flex justify-center",
plan.level === selectedPlan && "font-medium",
)}
>
{shouldShowCheck(feature.included, plan.level) ? (
<CheckIcon className="w-5 h-5 text-blue-500" />
) : (
<span className="text-zinc-300 dark:text-zinc-700">
-
</span>
)}
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
<div className="mt-8 text-center">
<Button
className={cn(
"w-full sm:w-auto bg-blue-500 hover:bg-blue-600 px-8 py-2 rounded-xl",
buttonClassName,
)}
>
Get started with {plans.find((p) => p.level === selectedPlan)?.name}
<ArrowRightIcon className="w-4 h-4 ml-2" />
</Button>
</div>
</div>
</section>
)
}
function shouldShowCheck(
included: PricingFeature["included"],
level: string,
): boolean {
if (included === "all") return true
if (included === "pro" && (level === "pro" || level === "all")) return true
if (
included === "starter" &&
(level === "starter" || level === "pro" || level === "all")
)
return true
return false
}
Browse & preview free Unlock everything — from $19/mo





