logo

Nurav UI

Community
Pricing Section

Bento Pricing

Dynamic pricing section featuring a Monthly vs Annually toggle with smooth Framer Motion layout animations.Copy Markdown

Preview

A unified border-card layout focusing on smooth context switching between monthly and annual plans. Uses layoutId for the toggle.

Loading...

Usage with Props

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>
  );
}

Installation

Automatic (CLI)

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:

components.json
{
  "registries": {
    "@nurav-ui": "https://nurav-ui.vercel.app/r"
  }
}

Or skip alias setup entirely and use the direct URL below.

Terminal
npx shadcn@latest add @nurav-ui/bento-pricing

Alternatively, install via direct URL (no components.json changes needed):

Terminal
npx shadcn@latest add https://nurav-ui.vercel.app/r/bento-pricing.json

Manual Setup

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:

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>
  );
};

Props

BentoPricingProps

PropTypeDefaultDescription
title
ReactNode"Pricing that scales with you"The main heading of the pricing section.
annualDiscountBadge
string"Save 20%"Text shown on the toggle to indicate annual savings.
tiers
BentoPricingTier[]defaultTiersArray of mapped pricing tiers.

BentoPricingTier

PropTypeDefaultDescription
namerequired
string—The title of the plan (e.g. "Startup").
descriptionrequired
string—A short description for the tier.
monthlyPricerequired
string—The price string applied when toggled to Monthly.
annualPricerequired
string—The price string applied when toggled to Annually.
featuresrequired
string[]—Checked functional features.
disabledFeatures
string[]—Features that are crossed out (X icon).
buttonTextrequired
string—CTA button copy.
isPopular
boolean—If true, highlights the column with a top stroke border.

Built by Varun

Last Updated:April 6, 2026

On this page