A Quasar pricing page is four sections stacked in a fixed order: a header with a billing toggle, the plan cards, an FAQ, and one last call to action. Each one maps to a handful of Quasar components you already know: q-btn-toggle, q-card, q-expansion-item and q-btn. This post builds all four with the Quasar 2.35.2 API, explains why the order matters, and then shows how to skip the hand-building by assembling the page from ready-made Components and theming it.
The plan names and numbers below are example data for a made-up product. Swap in your own.
Visitors land on a pricing page with one question: which plan is mine, and what does it cost? The order of the sections follows that question:
A long feature comparison table can sit between the cards and the FAQ if your plans differ in many small ways. For three plans with five differences each, the card lists are enough.
q-btn-toggle renders a group of buttons where exactly one is active, and its v-model holds the active option's value:
<script setup lang="ts">
import { computed, ref } from 'vue'
type Billing = 'monthly' | 'yearly'
const billing = ref<Billing>('monthly')
const billingOptions = [
{ label: 'Monthly', value: 'monthly' },
{ label: 'Yearly', value: 'yearly' },
]
const yearly = computed(() => billing.value === 'yearly')
</script>
<template>
<div class="text-center q-mb-xl">
<div class="text-overline text-primary">Pricing</div>
<h2 class="text-h4 q-my-sm">Plans that grow with your team</h2>
<q-btn-toggle
v-model="billing"
:options="billingOptions"
unelevated
no-caps
toggle-color="primary"
class="q-mt-md"
/>
<div class="text-caption q-mt-sm">Yearly billing: two months free</div>
</div>
</template>toggle-color colors the active button, and every other QBtn prop (unelevated, no-caps, rounded, padding) applies to all the buttons at once. An option object can carry its own QBtn props too, such as an icon or an attrs: { 'aria-label': ... }.
A q-toggle with false-value="monthly" and true-value="yearly" works as well, if you prefer a switch between two labels. The segmented button group makes both choices visible, which is why most pricing pages use it.
Three cards in a row on desktop, stacked on phones, is row plus col-12 col-md-4. q-col-gutter-lg adds the space between them:
<script setup lang="ts">
// ...the toggle code from above, plus:
const plans = [
{ name: 'Starter', monthly: 9, yearly: 90, summary: 'For one person shipping a side project.', cta: 'Start free', features: ['1 project', '1,000 events a month', 'Email support'] },
{ name: 'Team', monthly: 29, yearly: 290, summary: 'For small teams that ship every week.', cta: 'Start a trial', featured: true, features: ['10 projects', '50,000 events a month', 'Shared dashboards', 'Priority support'] },
{ name: 'Scale', monthly: 99, yearly: 990, summary: 'For companies with many products.', cta: 'Talk to us', features: ['Unlimited projects', '1M events a month', 'SSO', 'A named contact'] },
]
</script>
<template>
<div class="row q-col-gutter-lg items-stretch">
<div v-for="plan in plans" :key="plan.name" class="col-12 col-md-4">
<q-card flat bordered class="full-height column" :class="{ 'plan--featured': plan.featured }">
<q-card-section>
<div class="row items-center justify-between">
<div class="text-h6">{{ plan.name }}</div>
<q-badge v-if="plan.featured" color="primary" label="Most popular" />
</div>
<p class="text-body2 q-mt-sm">{{ plan.summary }}</p>
<div class="text-h3">${{ yearly ? plan.yearly : plan.monthly }}</div>
<div class="text-caption">{{ yearly ? 'per year' : 'per month' }}</div>
<q-btn
unelevated
no-caps
class="full-width q-mt-md"
:color="plan.featured ? 'primary' : 'grey-3'"
:text-color="plan.featured ? 'white' : 'dark'"
:label="plan.cta"
/>
</q-card-section>
<q-separator />
<q-card-section class="col">
<div v-for="feature in plan.features" :key="feature" class="row items-center no-wrap q-mb-sm">
<q-icon name="check" color="primary" size="18px" class="q-mr-sm" />
<span class="text-body2">{{ feature }}</span>
</div>
</q-card-section>
</q-card>
</div>
</div>
</template>
<style scoped>
.plan--featured {
border-color: var(--q-primary);
border-width: 2px;
}
</style>The details that matter:
items-stretch on the row and full-height column on the card give all three cards the same height. The feature section gets col, so it takes the leftover space and the buttons line up.computed, so switching the period updates every card at once.var(--q-primary), the CSS variable Quasar sets for the brand color. It follows your theme without hard-coding a hex value.q-expansion-item is a collapsible row. Give every item the same group, and opening one closes the others:
<script setup lang="ts">
const faqs = [
{ q: 'Can I cancel at any time?', a: 'Yes. Your plan stays active until the end of the period you paid for.' },
{ q: 'Do I need a card for the trial?', a: 'No. Add one only when you pick a paid plan.' },
{ q: 'What happens when I hit my event limit?', a: 'Nothing stops. We email you and you can upgrade or wait for the next month.' },
]
</script>
<template>
<q-list bordered separator class="rounded-borders" style="max-width: 720px; margin: 0 auto">
<q-expansion-item
v-for="(item, i) in faqs"
:key="item.q"
group="pricing-faq"
:label="item.q"
:default-opened="i === 0"
header-class="text-weight-medium"
>
<q-card>
<q-card-section class="text-body2">{{ item.a }}</q-card-section>
</q-card>
</q-expansion-item>
</q-list>
</template>Opening the first answer by default with default-opened tells the reader the rows are clickable. For more than eight or so questions, a two-column grid of always-visible answers reads faster than an accordion.
The last section repeats the main action. Keep it short: a heading, one sentence, one primary button, and at most one secondary link.
<template>
<div class="bg-primary text-white text-center q-pa-xl rounded-borders">
<h2 class="text-h5 q-mt-none q-mb-sm">Try it on your next project</h2>
<p class="q-mb-lg">Free for 14 days. No card needed.</p>
<q-btn unelevated no-caps color="white" text-color="primary" label="Start free" />
</div>
</template>Building the four sections by hand takes an afternoon. Assembling them from QuasarUI Components takes a few minutes, because each section exists as a Block:
q-btn-toggle, q-toggle or a pair of buttons. Three are free, including a three-card layout that shows the yearly price under the monthly one.q-expansion-item or as open grids. Two are free: one of each kind.Each Component is a single .vue file with the plan data in its <script setup>. Copy the three you picked into one page, stack them in the order from the first section of this post, and replace the example data with your own. The plan array is the only part you have to edit.
A pricing page shows your brand colors more than most pages do: the toggle, the featured card, every button. Every QuasarUI Component uses Quasar's brand color names (primary, secondary and so on), so setting those colors recolors the whole page.
The free Theme Builder goes further. Pick one of its 12 presets or build your own palette, fonts and radius, then export the CSS and config for Quasar CLI or Nuxt. Two Components on this page are Themeable Components: the last Pricing Section and the last CTA Section read the Theme Builder's surface, text, border, radius and font tokens. Their previews can switch between presets, so you can see the page in your theme before you copy anything.
How do I show yearly prices as a monthly amount? Store both numbers in the plan data and pick one with the toggle's computed, as above. Showing "per month, billed yearly" under the yearly figure avoids confusion about what gets charged.
How do I keep the plan cards the same height? Use items-stretch on the row, full-height column on the q-card, and col on the section that holds the feature list.
Should the toggle default to monthly or yearly? Both are common. Defaulting to yearly shows the lower monthly figure first. Defaulting to monthly matches what most visitors expect to pay first. Pick one and say clearly which period the price is for.
Can a pricing page be static in Nuxt? Yes. Nothing in these sections needs the server. Prerender the route if your plans only change on deploys.
Open the Pricing Sections Block, pick a layout with or without the billing switch, and copy the code. The free ones work without an account. Then add an FAQ and a CTA below it and you have the page.