Vue componentFree

Pricing Tiers

Three-tier Vue 3 pricing table with a monthly/yearly toggle, a highlighted plan and a select event. Tailwind CSS, dark mode, fully typed props.

by Vueplayv1.0.0Updated 3 hours ago

Install
Full screen ↗

About this component

A pricing section in the shape most SaaS products need: three plans side by side, the middle one highlighted, and a billing switch that recalculates prices as the visitor toggles.

Yearly prices can be set per plan or derived from a single discount, and the component emits select with the plan and billing period so you can route to checkout however you like. The toggle is an accessible radio group.

  • Monthly/yearly toggle with discount badge
  • Featured plan styling
  • Emits select(plan, billing)
  • Light and dark mode

Code

11 files in the repo
<script setup lang="ts">
import { computed, ref } from 'vue'

export interface PricingPlan {
  id: string
  name: string
  description: string
  /** Price per month when billed monthly, in whole currency units. */
  monthly: number
  /** Price per month when billed yearly. Omit to derive it from `yearlyDiscount`. */
  yearly?: number
  features: string[]
  cta?: string
  featured?: boolean
}

const props = withDefaults(defineProps<{
  plans?: PricingPlan[]
  heading?: string
  subheading?: string
  currency?: string
  /** Fraction taken off the monthly price when billed yearly, used when a plan has no `yearly`. */
  yearlyDiscount?: number
  defaultBilling?: 'monthly' | 'yearly'
}>(), {
  heading: 'Simple pricing that grows with you',
  subheading: 'Start free, upgrade when your team needs more. Every plan includes unlimited projects.',
  currency: '$',
  yearlyDiscount: 0.2,
  defaultBilling: 'monthly',
  plans: () => [
    { id: 'starter', name: 'Starter', description: 'For side projects and trying things out.', monthly: 0, features: ['Up to 3 projects', 'Community support', 'Basic analytics'], cta: 'Get started' },
    { id: 'pro', name: 'Pro', description: 'For professionals shipping real products.', monthly: 24, features: ['Unlimited projects', 'Custom domains', 'Priority email support', 'Advanced analytics'], cta: 'Start free trial', featured: true },
    { id: 'team', name: 'Team', description: 'For teams that collaborate every day.', monthly: 49, features: ['Everything in Pro', 'Shared workspaces', 'Roles and permissions', 'SSO and audit log'], cta: 'Contact sales' },
  ],
})

const emit = defineEmits<{
  select: [plan: PricingPlan, billing: 'monthly' | 'yearly']
}>()

const billing = ref<'monthly' | 'yearly'>(props.defaultBilling)

const priced = computed(() => props.plans.map((plan) => {
  const perMonth = billing.value === 'monthly' ? plan.monthly : (plan.yearly ?? Math.round(plan.monthly * (1 - props.yearlyDiscount)))
  return { plan, perMonth }
}))
</script>

<template>
  <section class="px-6 py-20 sm:py-24">
    <div class="mx-auto max-w-2xl text-center">
      <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl dark:text-white">{{ heading }}</h2>
      <p class="mt-4 text-lg text-gray-600 dark:text-gray-400">{{ subheading }}</p>
    </div>

    <div class="mt-10 flex justify-center">
      <div role="radiogroup" aria-label="Billing period" class="inline-flex rounded-full bg-gray-100 p-1 text-sm font-medium dark:bg-gray-800">
        <button
          v-for="option in (['monthly', 'yearly'] as const)"
          :key="option"
          type="button"
          role="radio"
          :aria-checked="billing === option"
          class="rounded-full px-4 py-2 transition"
          :class="billing === option ? 'bg-white text-gray-900 shadow-sm dark:bg-gray-950 dark:text-white' : 'text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white'"
          @click="billing = option"
        >
          {{ option === 'monthly' ? 'Monthly' : 'Yearly' }}
          <span v-if="option === 'yearly' && yearlyDiscount > 0" class="ml-1 rounded-full bg-emerald-100 px-2 py-0.5 text-xs text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-400">
            −{{ Math.round(yearlyDiscount * 100) }}%
          </span>
        </button>
      </div>
    </div>

    <div class="mx-auto mt-12 grid max-w-6xl gap-6 lg:grid-cols-3">
      <article
        v-for="{ plan, perMonth } in priced"
        :key="plan.id"
        class="relative flex flex-col rounded-2xl border p-8"
        :class="plan.featured
          ? 'border-indigo-600 bg-gray-900 text-white shadow-xl shadow-indigo-600/10 dark:border-indigo-400 dark:bg-gray-900'
          : 'border-gray-200 bg-white text-gray-900 dark:border-gray-800 dark:bg-gray-950 dark:text-white'"
      >
        <span v-if="plan.featured" class="absolute -top-3 left-8 rounded-full bg-indigo-600 px-3 py-1 text-xs font-semibold text-white dark:bg-indigo-500">Most popular</span>
        <h3 class="text-lg font-semibold">{{ plan.name }}</h3>
        <p class="mt-2 text-sm" :class="plan.featured ? 'text-gray-300' : 'text-gray-600 dark:text-gray-400'">{{ plan.description }}</p>
        <p class="mt-6 flex items-baseline gap-1">
          <span class="text-4xl font-bold tracking-tight">{{ perMonth === 0 ? 'Free' : `${currency}${perMonth}` }}</span>
          <span v-if="perMonth > 0" class="text-sm" :class="plan.featured ? 'text-gray-400' : 'text-gray-500'">/ month</span>
        </p>
        <p class="mt-1 h-5 text-xs" :class="plan.featured ? 'text-gray-400' : 'text-gray-500'">
          <template v-if="perMonth > 0 && billing === 'yearly'">{{ currency }}{{ perMonth * 12 }} billed yearly</template>
        </p>
        <ul class="mt-6 flex-1 space-y-3 text-sm">
          <li v-for="feature in plan.features" :key="feature" class="flex gap-3">
            <svg viewBox="0 0 20 20" aria-hidden="true" class="size-5 shrink-0" :class="plan.featured ? 'fill-indigo-400' : 'fill-indigo-600 dark:fill-indigo-400'">
              <path fill-rule="evenodd" d="M16.7 5.3a1 1 0 0 1 0 1.4l-8 8a1 1 0 0 1-1.4 0l-4-4a1 1 0 1 1 1.4-1.4L8 12.6l7.3-7.3a1 1 0 0 1 1.4 0Z" clip-rule="evenodd" />
            </svg>
            {{ feature }}
          </li>
        </ul>
        <button
          type="button"
          class="mt-8 rounded-xl px-4 py-2.5 text-sm font-semibold transition focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
          :class="plan.featured
            ? 'bg-indigo-500 text-white hover:bg-indigo-400'
            : 'bg-gray-900 text-white hover:bg-gray-700 dark:bg-white dark:text-gray-900 dark:hover:bg-gray-200'"
          @click="emit('select', plan, billing)"
        >
          {{ plan.cta ?? 'Choose plan' }}
        </button>
      </article>
    </div>
  </section>
</template>

Props

PropTypeDefaultDescription
plansPricingPlan[]Starter, Pro, TeamPlans to show. Each has id, name, description, monthly price, optional yearly price, features, cta label and featured flag.
headingstring'Simple pricing that grows with you'Section heading.
subheadingstring'Start free, upgrade when…'Supporting text under the heading.
currencystring'$'Currency symbol put before prices.
yearlyDiscountnumber0.2Discount applied to the monthly price for yearly billing when a plan has no explicit yearly price.
defaultBilling'monthly' | 'yearly''monthly'Billing period selected on first render.

Events

EventPayloadDescription
select(plan: PricingPlan, billing: 'monthly' | 'yearly')Fired when a plan's button is clicked.
All pricing tables →
Preview of Pricing ComparisonFree

Component · Pricing tables

Pricing Comparison

Vue 3 plan comparison table with grouped feature rows, check and dash cells, a sticky plan header on desktop and stacked cards on mobile. Tailwind CSS.

by Vueplay
Preview of Pricing SingleFree

Component · Pricing tables

Pricing Single

Single-plan Vue 3 pricing card with a feature checklist, a sale price, a money-back guarantee and one clear CTA. Tailwind CSS, dark mode, typed props.

by Vueplay
Preview of Hero SplitFree

Component · Hero sections

Hero Split

Split Vue 3 hero section with copy and two CTAs on the left and a Tailwind-drawn app window mockup on the right. Dark mode, typed props, no images.

by Vueplay1 installs
Preview of Bento GridFree

Component · Feature sections

Bento Grid

Bento-style Vue 3 feature grid with wide and tall tiles and small CSS-drawn visuals like maps, timelines and code. Tailwind CSS, dark mode, typed tiles.

by Vueplay

Pro feature

Upgrade to Pro

Upgrade to unlock more of Vueplay. Cancel any time.

  • Custom domains with HTTPS
  • Private projects
  • No "Made with Vueplay" badge
  • GitHub sync

$120/year

Compare plansBilling settings