Pricing blocks
Plan tables for a pricing page or an upgrade screen, built from SvCard,
SvButton, SvBadge, SvSegmented and SvNumberInput. Prices are plain data:
the PLANS array at the top of each file is the default for the core's plans
option, so changing a plan is an edit to one array, or pass your own. The newer
blocks keep money in integer cents and format it with Intl.NumberFormat
(currency and locale options, US dollars and en-US by default).
Installation
See them first with npx @svgrid/ui try blocks/pricing.
Three tiers with a billing toggle
Three plan cards with a monthly / annual SvSegmented switch. The annual price
applies the discount as you toggle. The middle plan is highlighted, and each
card lists its features with check marks and ends in a call-to-action button.
Open the live example: Pricing block (Blocks)
npx @svgrid/ui add pricing
The core also takes annualDiscount (a fraction, 0.2 by default, which also
sets the "Annual -20%" label), currency and onChoose(plan, cycle), which is
where a call-to-action goes to checkout or a sales form.
Highlighted plan with a seat count
Four per-seat plans with the recommended one outlined and badged. The Seats
SvNumberInput reprices every card: from 25 seats each seat is 10% off and
from 100 seats 20% off, with the list price struck through and the monthly
total under it. Starter allows up to 5 seats, so above that its button turns
off. Enterprise has no seat price and shows "Custom" with a sales button.
npx @svgrid/ui add pricing-highlighted
createSeatPricing() takes plans (a plan with seatCents: null is
quote-only, maxSeats caps a plan), volume (bands of { from, off }),
seats, maxSeats for the field and onChoose(plan, seats, totalCents).
The spin buttons and arrow keys reprice at once; a typed count applies on
Enter or when the field loses focus.
Comparison table
A feature-by-plan table with grouped sections. Included features get a check
mark, missing ones a dash, and limits show as text. The header row with each
plan's name, price and button is sticky, so it stays in view while you scroll
the features. Below 640 px the table shows one plan column at a time and an
SvSegmented above it picks the plan.
npx @svgrid/ui add pricing-comparison
createPlanComparison() takes plans and sections. A feature row is
{ name, values }, where values maps a plan id to true, false or a
string. The header sticks to the nearest scrolling container at top: 0; with
a fixed navbar, set top to its height.
Loading plans from your billing provider
Each core takes its plans as an option, so prices can come from your billing
provider instead of the file. In SvelteKit, read them in +page.server.ts and
map them to the block's plan shape. listPrices stands for your provider's
client call; keep amounts in the smallest currency unit, which is what most
providers return:
// src/routes/pricing/+page.server.ts
import { listPrices } from '$lib/server/billing'
export const load = async () => {
const prices = await listPrices({ active: true })
return {
plans: prices.map((p) => ({
id: p.id,
name: p.productName,
blurb: p.description,
seatCents: p.unitAmount,
features: p.features,
cta: `Choose ${p.productName}`,
recommended: p.metadata.recommended === 'true',
})),
}
}
Then pass data.plans to the core in the block's instance script, and send
the choice to a checkout endpoint of your own:
<script lang="ts">
let { data } = $props()
const pricing = createSeatPricing({
plans: data.plans,
onChoose: (plan, seats) => location.assign(`/checkout?price=${plan.id}&quantity=${seats}`),
})
</script>
Headless
The block is a styled render of createPricing(), the core in its
<script module>. To draw your own markup, take the core alone with
npx @svgrid/ui add pricing --headless (it writes pricing.svelte.ts):
<script lang="ts">
import { createPricing } from './pricing.svelte.js'
const pricing = createPricing({
annualDiscount: 0.15,
onChoose: (plan, cycle) => location.assign(`/checkout?plan=${plan.id}&cycle=${cycle}`),
})
</script>
<div role="group" aria-label="Billing">
<button {...pricing.cycleProps('monthly')}>Monthly</button>
<button {...pricing.cycleProps('annual')}>Annual</button>
</div>
{#each pricing.plans as plan (plan.id)}
<article>
<h3>{plan.name}</h3>
<p>{pricing.price(plan)} / month</p>
<button onclick={() => pricing.choose(plan)}>{plan.cta}</button>
</article>
{/each}
See also
Live examples
- Pricing block - A three-tier plan grid with a monthly/annual SvSegmented toggle (annual applies the discount live), a highlighted "most popular" card, per-plan feature SvLists with check marks, and a CTA SvButton on each.
- Pricing with highlighted plan block - Four per-seat plans in SvCards with the recommended one outlined and badged. A Seats SvNumberInput reprices every card live, with volume discounts at 25 and 100 seats, monthly totals, and seat caps that switch a plan off.
- Pricing comparison table block - A feature-by-plan table with grouped sections, check marks and limits, and a sticky header row holding each plan's price and SvButton. On phones an SvSegmented picks which plan column is shown.