Pricing Section
Bento Pricing Dynamic pricing section featuring a Monthly vs Annually toggle with smooth Framer Motion layout animations. Copy Markdown
A unified border-card layout focusing on smooth context switching between monthly and annual plans. Uses layoutId for the toggle.
You can customize the bento pricing section by passing your own tiers. This is specifically designed for components that require a monthly/yearly toggle.
import { BentoPricing, BentoPricingTier } from "@/components/nurav-ui/BentoPricing" ;
export default function App () {
const tiers : BentoPricingTier [] = [
{
name: "Basic" ,
description: "For individuals." ,
monthlyPrice: "$10" ,
annualPrice: "$8" ,
features: [ "Up to 3 Projects" , "Basic Analytics" ],
disabledFeatures: [ "Custom Domains" , "24/7 Support" ],
buttonText: "Start Basic" ,
},
{
name: "Pro" ,
description: "For scale-ups." ,
monthlyPrice: "$40" ,
annualPrice: "$32" ,
features: [ "Unlimited Projects" , "Custom Domains" , "Priority Support" ],
buttonText: "Start Pro" ,
isPopular: true ,
}
];
return (
< div className = "w-full" >
< BentoPricing
title = "Pricing that scales with you"
annualDiscountBadge = "Save 20%"
tiers = {tiers}
/>
</ div >
);
}
The fastest way to install this pricing block is using the setup CLI.
Before using the @nurav-ui alias , add this to your components.json:
{
"registries" : {
"@nurav-ui" : "https://nurav-ui.vercel.app/r"
}
} Or skip alias setup entirely and use the direct URL below.
npx shadcn@latest add @nurav-ui/bento-pricing
Alternatively , install via direct URL (no components.json changes needed):
npx shadcn@latest add https://nurav-ui.vercel.app/r/bento-pricing.json
If you prefer building it manually, you can just copy and paste the code below directly into your project.
Create the Component Add the following code to components/nurav-ui/BentoPricing.tsx:
"use client" ;
import React, { useState } from "react" ;
import { motion, AnimatePresence } from "motion/react" ;
import { Check, X } from "lucide-react" ;
import { cn } from "@/lib/utils" ;
export interface BentoPricingTier {
name : string ;
description : string ;
monthlyPrice : string ;
annualPrice : string ;
features : string [];
disabledFeatures ?: string [];
buttonText : string ;
isPopular ?: boolean ;
}
export interface BentoPricingProps {
title ?: React . ReactNode ;
annualDiscountBadge ?: string ;
tiers ?: BentoPricingTier [];
}
export const BentoPricing = ({
title = "Pricing that scales with you" ,
annualDiscountBadge = "Save 20%" ,
tiers,
} : BentoPricingProps ) => {
const [ isAnnual , setIsAnnual ] = useState ( true );
return (
< section className = "w-full py-8 px-3 bg-background" >
< div className = "max-w-5xl mx-auto" >
< div className = "text-center mb-12" >
< h2 className = "text-4xl font-extrabold tracking-tight text-foreground mb-6" >
{title}
</ h2 >
{ /* Toggle */ }
< div className = "flex items-center justify-center gap-2" >
< span
className = { cn (
"text-sm font-semibold transition-colors" ,
! isAnnual ? "text-foreground" : "text-muted-foreground" ,
)}
>
Monthly
</ span >
< button
onClick = {() => setIsAnnual ( ! isAnnual)}
className = "relative w-12 h-6 rounded-full bg-foreground/10 p-1 transition-colors hover:bg-foreground/20"
>
< motion.div
className = "w-4 h-4 bg-primary rounded-full shadow-md"
animate = {{ x: isAnnual ? 24 : 0 }}
transition = {{ type: "spring" , stiffness: 500 , damping: 30 }}
/>
</ button >
< div className = "flex items-center gap-2 ml-2" >
< span
className = { cn (
"text-sm font-semibold transition-colors" ,
isAnnual ? "text-foreground" : "text-muted-foreground" ,
)}
>
Annually
</ span >
{annualDiscountBadge && (
< span className = "text-xs font-bold tracking-wider uppercase bg-primary/10 text-primary px-2 py-0.5 rounded-full" >
{annualDiscountBadge}
</ span >
)}
</ div >
</ div >
</ div >
< div
style = {{
gridTemplateColumns: `repeat(${ tiers ?. length },minmax(0,1fr))` ,
}}
className = "md:grid gap-0 border border-foreground/10 rounded-3xl overflow-hidden shadow-xl bg-card"
>
{tiers?. map (( tier , idx ) => {
const isPopular = tier.isPopular;
return (
< div
key = {idx}
className = { cn (
"p-8 relative" ,
idx !== tiers. length - 1 &&
"border-b md:border-b-0 md:border-r border-foreground/10" ,
isPopular ? "bg-primary/3" : "" ,
)}
>
{isPopular && (
< div className = "absolute top-0 inset-x-0 h-1 bg-primary" />
)}
< h3
className = { cn (
"text-xl font-bold mb-2" ,
isPopular ? "text-primary" : "" ,
)}
>
{tier.name}
</ h3 >
< p className = "text-sm text-muted-foreground min-h-[40px] mb-6" >
{tier.description}
</ p >
< div className = "mb-6 h-[60px]" >
< div className = "flex items-baseline" >
< AnimatePresence mode = "popLayout" >
< motion.span
key = {isAnnual ? "annual" : "monthly" }
initial = {{ opacity: 0 , y: - 20 }}
animate = {{ opacity: 1 , y: 0 }}
exit = {{ opacity: 0 , y: 20 }}
className = "text-4xl font-black"
>
{isAnnual ? tier.annualPrice : tier.monthlyPrice}
</ motion.span >
</ AnimatePresence >
< span className = "text-muted-foreground ml-1" >/mo</ span >
</ div >
</ div >
< button
className = { cn (
"w-full py-3 rounded-lg font-semibold transition-all mb-8" ,
isPopular
? "bg-primary text-primary-foreground hover:opacity-90"
: "border-2 border-foreground/10 text-foreground hover:bg-foreground/5" ,
)}
>
{tier.buttonText}
</ button >
< ul className = "space-y-3 text-sm" >
{tier.features. map (( ft , i ) => (
< li
key = { `ft-${ i }` }
className = "flex items-center gap-3 text-foreground/80"
>
< Check className = "w-4 h-4 text-primary" /> {ft}
</ li >
))}
{tier.disabledFeatures?. map (( ft , i ) => (
< li
key = { `dft-${ i }` }
className = "flex items-center gap-3 text-muted-foreground/50"
>
< X className = "w-4 h-4" /> {ft}
</ li >
))}
</ ul >
</ div >
);
})}
</ div >
</ div >
</ section >
);
};
Last Updated: April 6, 2026