# Pricing Section One

> A responsive three-tier pricing section with a monthly and yearly billing switcher.

A shadcn/ui block from the [@navui](https://ui.navdeepsingh.dev) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/navui/pricing-section-one).

- Page, with a live preview: https://shadcn.exchange/r/navui/pricing-section-one
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/navui/pricing-section-one-light-3413aa54e2f19621.png
- Registry item: https://ui.navdeepsingh.dev/r/pricing-section-one.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://ui.navdeepsingh.dev/r/pricing-section-one.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `badge`, `button`, `card`, `tabs`

## Files

### `src/registry/blocks/pricing/pricing-section-one/pricing-section-one.tsx`

```tsx
'use client'

import type { ComponentPropsWithoutRef } from 'react'
import { useState } from 'react'
import { Check } from 'lucide-react'

import { AnimatedNumber } from '@/components/animated-numbers'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from '@/components/ui/card'
import { cn } from '@/lib/utils'

export type PricingInterval = 'monthly' | 'yearly'

export type PricingValue = number | string

// Defines the content and configuration for each pricing plan.
export type PricingPlan = {
  id: string
  name: string
  description: string
  monthlyPrice: PricingValue
  yearlyPrice: PricingValue
  features: readonly string[]
  ctaLabel: string
  ctaHref: string
  prefix?: string
  priceSuffix?: string
  featured?: boolean
  badge?: string
}

// Controls the section heading content.
export type PricingSectionCopy = {
  eyebrow?: string
  heading: string
  description: string
}

// Controls the billing toggle labels.
export type PricingBillingCopy = {
  ariaLabel: string
  monthlyLabel: string
  yearlyLabel: string
  yearlyNote: string
}

export type PricingSectionOneProps = ComponentPropsWithoutRef<'section'> & {
  plans?: readonly PricingPlan[]
  defaultInterval?: PricingInterval
  copy?: Partial<PricingSectionCopy>
  billing?: Partial<PricingBillingCopy>
}

// Default section content.
const defaultCopy: PricingSectionCopy = {
  heading: 'Simple pricing for every stage',
  description:
    'Start for free, upgrade when you need more, or contact us for a plan tailored to your organization.',
}

// Default billing toggle content.
const defaultBilling: PricingBillingCopy = {
  ariaLabel: 'Choose billing interval',
  monthlyLabel: 'Monthly',
  yearlyLabel: 'Yearly',
  yearlyNote: 'Save 20%',
}

// Default pricing plans used when no custom plans are provided.
const defaultPlans: readonly PricingPlan[] = [
  {
    id: 'free',
    name: 'Free',
    description: 'For individuals getting started with the essential tools.',
    prefix: '$',
    monthlyPrice: 0,
    yearlyPrice: 0,
    priceSuffix: '/ month',
    features: ['1 workspace', 'Up to 3 projects', 'Basic analytics', 'Community support'],
    ctaLabel: 'Start free',
    ctaHref: '#get-started',
  },
  {
    id: 'pro',
    name: 'Pro',
    description: 'For professionals and growing teams that need more power and flexibility.',
    prefix: '$',
    monthlyPrice: 29,
    yearlyPrice: 23,
    priceSuffix: '/ month',
    features: [
      'Unlimited projects',
      'Advanced analytics',
      'Automations and integrations',
      'Custom branding',
      'Priority support',
    ],
    ctaLabel: 'Start 7-day free trial',
    ctaHref: '#get-started',
    featured: true,
    badge: 'Most popular',
  },
  {
    id: 'enterprise',
    name: 'Enterprise',
    description: 'For organizations that need advanced security, support, and customization.',
    monthlyPrice: 'Custom',
    yearlyPrice: 'Custom',
    features: [
      'Everything in Pro',
      'Unlimited workspaces',
      'Advanced permissions',
      'Single sign-on',
      'Dedicated support',
      'Custom contracts',
    ],
    ctaLabel: 'Talk to sales',
    ctaHref: '#contact-sales',
  },
]

type PricingCardProps = {
  interval: PricingInterval
  plan: PricingPlan
}

// Renders a single pricing plan.
function PricingCard({ interval, plan }: PricingCardProps) {
  // Select the price based on the active billing interval.
  const price = interval === 'yearly' ? plan.yearlyPrice : plan.monthlyPrice
  const isNumericPrice = typeof price === 'number'

  return (
    <Card
      className={cn(
        'relative flex h-full flex-col gap-0 overflow-visible py-0',
        plan.featured && 'border-primary/40 shadow-lg ring-1 shadow-primary/5 ring-primary/20',
      )}
    >
      {/* Optional plan badge. */}
      {plan.badge ? (
        <Badge
          variant={plan.featured ? 'default' : 'secondary'}
          className="absolute inset-x-0 -top-3 left-1/2 shrink-0 -translate-x-1/2 gap-1.5"
        >
          {plan.badge}
        </Badge>
      ) : null}

      {/* Plan name and description. */}
      <CardHeader className="gap-4 px-6 pt-6 pb-5">
        <CardTitle className="text-lg">{plan.name}</CardTitle>
        <CardDescription className="leading-6 text-balance">{plan.description}</CardDescription>
      </CardHeader>

      <CardContent className="flex flex-1 flex-col border-t border-dashed px-6 py-6">
        <div aria-live="polite" aria-atomic="true" className="flex min-h-12 items-end gap-1.5">
          {isNumericPrice ? (
            <AnimatedNumber
              value={price}
              prefix={plan.prefix}
              className="text-4xl font-semibold tracking-tight text-foreground"
            />
          ) : (
            <span className="text-4xl font-semibold tracking-tight text-foreground">{price}</span>
          )}

          {isNumericPrice && plan.priceSuffix ? (
            <span className="pb-1 text-sm text-muted-foreground">{plan.priceSuffix}</span>
          ) : null}
        </div>

        {/* Included plan features. */}
        <ul className="mt-7 space-y-3">
          {plan.features.map((feature) => (
            <li key={feature} className="flex items-start gap-2.5 text-sm text-muted-foreground">
              <span
                aria-hidden="true"
                className="mt-0.5 grid size-5 shrink-0 place-items-center rounded-full bg-primary/10 text-primary"
              >
                <Check className="size-3" />
              </span>

              <span className="leading-5">{feature}</span>
            </li>
          ))}
        </ul>
      </CardContent>

      {/* Plan call to action. */}
      <CardFooter className="mt-auto px-6 py-6">
        <Button
          asChild
          variant={plan.featured ? 'default' : 'outline'}
          size="lg"
          className="w-full"
        >
          <a href={plan.ctaHref}>{plan.ctaLabel}</a>
        </Button>
      </CardFooter>
    </Card>
  )
}

export function PricingSectionOne({
  className,
  plans = defaultPlans,
  defaultInterval = 'monthly',
  copy,
  billing,
  ...props
}: PricingSectionOneProps) {
  // Tracks the active billing interval.
  const [interval, setInterval] = useState<PricingInterval>(defaultInterval)

  // Merge custom copy with the defaults.
  const resolvedCopy: PricingSectionCopy = {
    ...defaultCopy,
    ...copy,
  }

  // Merge custom billing labels with the defaults.
  const resolvedBilling: PricingBillingCopy = {
    ...defaultBilling,
    ...billing,
  }

  return (
    <section
      data-slot="pricing-section-one"
      className={cn('bg-background py-20 text-foreground sm:py-28', className)}
      {...props}
    >
      <div className="mx-auto w-full max-w-6xl px-6">
        {/* Section introduction. */}
        <div className="mx-auto max-w-2xl text-center">
          {resolvedCopy.eyebrow ? <Badge variant="outline">{resolvedCopy.eyebrow}</Badge> : null}

          <h2 className="mt-5 text-3xl font-semibold tracking-tight text-balance sm:text-5xl">
            {resolvedCopy.heading}
          </h2>

          {resolvedCopy.description ? (
            <p className="mt-4 text-base leading-7 text-balance text-muted-foreground sm:text-lg">
              {resolvedCopy.description}
            </p>
          ) : null}
        </div>

        {/* Monthly and yearly billing toggle. */}
        <div className="mt-8 flex justify-center sm:mt-10">
          <div
            role="group"
            aria-label={resolvedBilling.ariaLabel}
            className="inline-flex rounded-lg bg-muted p-1"
          >
            <button
              type="button"
              aria-pressed={interval === 'monthly'}
              onClick={() => setInterval('monthly')}
              className={cn(
                'inline-flex min-h-9 items-center justify-center rounded-md px-4 py-1.5 text-sm font-medium text-muted-foreground',
                'transition-[background-color,color,box-shadow] duration-150',
                'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:outline-none',
                interval === 'monthly' && 'bg-background text-foreground shadow-sm',
              )}
            >
              {resolvedBilling.monthlyLabel}
            </button>

            <button
              type="button"
              aria-pressed={interval === 'yearly'}
              onClick={() => setInterval('yearly')}
              className={cn(
                'inline-flex min-h-9 items-center justify-center gap-2 rounded-md px-4 py-1.5 text-sm font-medium text-muted-foreground',
                'transition-[background-color,color,box-shadow] duration-150',
                'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:outline-none',
                interval === 'yearly' && 'bg-background text-foreground shadow-sm',
              )}
            >
              <span>{resolvedBilling.yearlyLabel}</span>

              {resolvedBilling.yearlyNote ? (
                <span
                  className={cn(
                    'text-xs',
                    interval === 'yearly' ? 'text-primary' : 'text-muted-foreground',
                  )}
                >
                  {resolvedBilling.yearlyNote}
                </span>
              ) : null}
            </button>
          </div>
        </div>

        {/* Pricing plan grid. */}
        <div className="mt-8 grid w-full gap-5 md:grid-cols-2 lg:grid-cols-3">
          {plans.map((plan) => (
            <PricingCard key={plan.id} plan={plan} interval={interval} />
          ))}
        </div>
      </div>
    </section>
  )
}

export default PricingSectionOne
```
