Pricing page
The page that sells the plan. A billing-period switch built on
md-segmented-button-set drives the prices in
three plan md-cards — the popular one carrying an
md-badge and highlight
md-chips — above a full
md-table comparison whose fine-print rows explain
themselves through md-tooltip, and a FAQ
md-accordion that answers the objections before
sales has to.
- 10 projects
- 3 team members
- Community support
- Unlimited projects
- 25 team members
- Unlimited everything
- 99.95% uptime SLA
- Dedicated support
Yes — upgrades apply immediately and are prorated; downgrades take effect at the next renewal.
Your workspace becomes read-only. Nothing is deleted for 90 days, and picking any plan restores it instantly.
Annual Pro and Scale plans can pay by invoice with net-30 terms. Monthly plans are card-only.
Annual plans can be refunded pro rata within 30 days of purchase. Contact billing@example.com with your invoice number.
Show code for each technology
<!-- index.html <head> — the icon font the components draw from -->
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap">
<!-- index.html — register the AWC UI elements once -->
<script type="module">
import '@awc-ui/core/define';
</script>
<md-segmented-button-set aria-label="Billing period">
<md-segmented-button value="monthly" label="Monthly" selected></md-segmented-button>
<md-segmented-button value="annual" label="Annual"></md-segmented-button>
</md-segmented-button-set>
<md-card variant="outlined" style="position: relative;">
<md-badge icon="star"
style="--md-badge-offset-x: -14px; --md-badge-offset-y: 14px;"></md-badge>
<h3>Pro · Most popular</h3>
<p><strong>$49</strong> per month, billed monthly</p>
<div role="group" aria-label="Pro plan highlights">
<md-chip variant="assist" label="SSO"></md-chip>
<md-chip variant="assist" label="Priority support"></md-chip>
</div>
<md-button variant="filled" full-width>Choose Pro</md-button>
</md-card>
<md-table-container variant="outlined">
<md-table column-template="1.5fr 1fr 1fr 1fr" label="Plan comparison" striped>
<md-table-head>
<md-table-row rowgroup="head">
<md-table-cell head scope="col">Feature</md-table-cell>
<md-table-cell head scope="col" align="center">Pro</md-table-cell>
</md-table-row>
</md-table-head>
<md-table-body>
<md-table-row value="sla">
<md-table-cell head scope="row">
<md-tooltip text="Measured per calendar month; credits apply automatically.">
<span tabindex="0">Uptime SLA</span>
</md-tooltip>
</md-table-cell>
<md-table-cell align="center">99.9%</md-table-cell>
</md-table-row>
</md-table-body>
</md-table>
</md-table-container>
<md-accordion exclusive heading-level="3" variant="outlined">
<md-accordion-item headline="Can I change plans later?">
<p>Yes — upgrades apply immediately and are prorated.</p>
</md-accordion-item>
</md-accordion>
<script type="module">
const billing = document.querySelector('[data-billing]');
const prices = document.querySelectorAll('[data-price]');
const terms = document.querySelectorAll('[data-term]');
const savings = document.querySelector('[data-savings]');
// Single-select mdChange always reports exactly one value, and it fires on
// every activation (including a re-click of the selected segment), so the
// handler is written to be idempotent.
billing.addEventListener('mdChange', (e) => {
const annual = e.detail[0] === 'annual';
prices.forEach((el) => {
el.textContent = '$' + (annual ? el.dataset.annual : el.dataset.monthly);
});
terms.forEach((el) => {
el.textContent = annual
? ' per month, billed annually'
: ' per month, billed monthly';
});
savings.textContent = annual
? 'You are saving 20% with annual billing'
: 'Save 20% with annual billing';
});
</script>import { useState } from 'react';
import {
MdAccordion, MdAccordionItem, MdBadge, MdButton, MdCard, MdChip,
MdSegmentedButton, MdSegmentedButtonSet, MdTable, MdTableBody, MdTableCell,
MdTableContainer, MdTableHead, MdTableRow, MdTooltip,
} from '@awc-ui/react';
const PLANS = [
{ name: 'Starter', monthly: 19, annual: 15, popular: false, variant: 'outlined', cta: 'Start free trial', features: ['10 projects', '3 team members', 'Community support'] },
{ name: 'Pro · Most popular', monthly: 49, annual: 39, popular: true, variant: 'filled', cta: 'Choose Pro', features: ['Unlimited projects', '25 team members'] },
{ name: 'Scale', monthly: 99, annual: 79, popular: false, variant: 'outlined', cta: 'Contact sales', features: ['Unlimited everything', '99.95% uptime SLA', 'Dedicated support'] },
];
const finePrint = { textDecoration: 'underline dotted', textUnderlineOffset: 3, cursor: 'help' };
export default function PricingPage() {
const [annual, setAnnual] = useState(false);
const term = annual ? ' per month, billed annually' : ' per month, billed monthly';
return (
<div style={{ display: 'grid', gap: 28 }}>
<div style={{ display: 'grid', justifyItems: 'center', gap: 12, textAlign: 'center' }}>
<span style={{ font: 'var(--md-sys-typescale-headline-small)' }}>Plans that grow with your team</span>
{/* Single-select mdChange always reports exactly one value in e.detail[0]. */}
<MdSegmentedButtonSet aria-label="Billing period" onMdChange={(e) => setAnnual(e.detail[0] === 'annual')}>
<MdSegmentedButton value="monthly" label="Monthly" selected={!annual} />
<MdSegmentedButton value="annual" label="Annual" selected={annual} />
</MdSegmentedButtonSet>
<span style={{ font: 'var(--md-sys-typescale-body-small)', color: 'var(--md-sys-color-on-surface-variant)' }}>
{annual ? 'You are saving 20% with annual billing' : 'Save 20% with annual billing'}
</span>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 16, alignItems: 'stretch' }}>
{PLANS.map((p) => (
<MdCard
key={p.name}
variant="outlined"
style={p.popular ? { position: 'relative', '--md-card-outline-color': 'var(--md-sys-color-primary)', '--md-card-outline-width': '2px' } : undefined}
>
{p.popular && <MdBadge icon="star" style={{ '--md-badge-offset-x': '-14px', '--md-badge-offset-y': '14px' }} />}
<span style={{ font: 'var(--md-sys-typescale-title-medium)' }}>{p.name}</span>
<div>
<span style={{ font: 'var(--md-sys-typescale-display-small)' }}>{'$' + (annual ? p.annual : p.monthly)}</span>
<span style={{ font: 'var(--md-sys-typescale-body-small)', color: 'var(--md-sys-color-on-surface-variant)' }}>{term}</span>
</div>
{p.popular && (
<div role="group" aria-label="Pro plan highlights" style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
<MdChip variant="assist" label="SSO" density={-2} />
<MdChip variant="assist" label="API access" density={-2} />
<MdChip variant="assist" label="Priority support" density={-2} />
</div>
)}
<ul style={{ margin: 0, padding: 0, listStyle: 'none', display: 'grid', gap: 6 }}>
{p.features.map((f) => <li key={f}>{f}</li>)}
</ul>
<MdButton variant={p.variant} fullWidth style={{ marginTop: 'auto' }}>{p.cta}</MdButton>
</MdCard>
))}
</div>
<MdTableContainer variant="outlined">
<MdTable columnTemplate="1.5fr 1fr 1fr 1fr" label="Plan comparison" striped>
<MdTableHead>
<MdTableRow rowgroup="head">
<MdTableCell head scope="col">Feature</MdTableCell>
<MdTableCell head scope="col" align="center">Starter</MdTableCell>
<MdTableCell head scope="col" align="center">Pro</MdTableCell>
<MdTableCell head scope="col" align="center">Scale</MdTableCell>
</MdTableRow>
</MdTableHead>
<MdTableBody>
<MdTableRow value="projects">
<MdTableCell head scope="row">Projects</MdTableCell>
<MdTableCell align="center">10</MdTableCell>
<MdTableCell align="center">Unlimited</MdTableCell>
<MdTableCell align="center">Unlimited</MdTableCell>
</MdTableRow>
<MdTableRow value="sla">
<MdTableCell head scope="row">
<MdTooltip text="Measured per calendar month; service credits apply automatically.">
<span tabIndex={0} style={finePrint}>Uptime SLA</span>
</MdTooltip>
</MdTableCell>
<MdTableCell align="center">—</MdTableCell>
<MdTableCell align="center">99.9%</MdTableCell>
<MdTableCell align="center">99.95%</MdTableCell>
</MdTableRow>
</MdTableBody>
</MdTable>
</MdTableContainer>
<div style={{ display: 'grid', gap: 12 }}>
<span style={{ font: 'var(--md-sys-typescale-title-medium)' }}>Frequently asked questions</span>
<MdAccordion exclusive headingLevel={3} variant="outlined">
<MdAccordionItem headline="Can I change plans later?">
<p style={{ margin: 0 }}>Yes — upgrades apply immediately and are prorated; downgrades take effect at the next renewal.</p>
</MdAccordionItem>
<MdAccordionItem headline="What happens when my trial ends?">
<p style={{ margin: 0 }}>Your workspace becomes read-only. Nothing is deleted for 90 days, and picking any plan restores it instantly.</p>
</MdAccordionItem>
</MdAccordion>
</div>
</div>
);
}import { Component } from '@angular/core';
import {
MdAccordion, MdAccordionItem, MdBadge, MdButton, MdCard, MdChip,
MdSegmentedButton, MdSegmentedButtonSet, MdTable, MdTableBody, MdTableCell,
MdTableContainer, MdTableHead, MdTableRow, MdTooltip,
} from '@awc-ui/angular';
@Component({
selector: 'app-pricing-page',
standalone: true,
imports: [
MdSegmentedButtonSet, MdSegmentedButton, MdCard, MdBadge, MdChip, MdButton,
MdTableContainer, MdTable, MdTableHead, MdTableBody, MdTableRow, MdTableCell,
MdTooltip, MdAccordion, MdAccordionItem,
],
template:
'<div class="pricing">' +
' <div class="pricing-head">' +
' <span class="pricing-title">Plans that grow with your team</span>' +
' <md-segmented-button-set aria-label="Billing period" (mdChange)="onBilling($event)">' +
' <md-segmented-button value="monthly" label="Monthly" [selected]="!annual"></md-segmented-button>' +
' <md-segmented-button value="annual" label="Annual" [selected]="annual"></md-segmented-button>' +
' </md-segmented-button-set>' +
' <span class="pricing-savings">{{ savings }}</span>' +
' </div>' +
' <div class="pricing-grid">' +
' @for (p of plans; track p.name) {' +
' <md-card variant="outlined" [class.popular]="p.popular">' +
' @if (p.popular) { <md-badge icon="star" class="pop-badge"></md-badge> }' +
' <span class="plan-name">{{ p.name }}</span>' +
' <div><span class="plan-price">{{ price(p) }}</span><span class="plan-term">{{ term }}</span></div>' +
' @if (p.popular) {' +
' <div role="group" aria-label="Pro plan highlights" class="plan-chips">' +
' <md-chip variant="assist" label="SSO" [density]="-2"></md-chip>' +
' <md-chip variant="assist" label="API access" [density]="-2"></md-chip>' +
' <md-chip variant="assist" label="Priority support" [density]="-2"></md-chip>' +
' </div>' +
' }' +
' <ul class="plan-features">@for (f of p.features; track f) { <li>{{ f }}</li> }</ul>' +
' <md-button [variant]="p.variant" [fullWidth]="true" class="plan-cta">{{ p.cta }}</md-button>' +
' </md-card>' +
' }' +
' </div>' +
' <md-table-container variant="outlined">' +
' <md-table columnTemplate="1.5fr 1fr 1fr 1fr" label="Plan comparison" [striped]="true">' +
' <md-table-head>' +
' <md-table-row rowgroup="head">' +
' <md-table-cell [head]="true" scope="col">Feature</md-table-cell>' +
' <md-table-cell [head]="true" scope="col" align="center">Starter</md-table-cell>' +
' <md-table-cell [head]="true" scope="col" align="center">Pro</md-table-cell>' +
' <md-table-cell [head]="true" scope="col" align="center">Scale</md-table-cell>' +
' </md-table-row>' +
' </md-table-head>' +
' <md-table-body>' +
' <md-table-row value="projects">' +
' <md-table-cell [head]="true" scope="row">Projects</md-table-cell>' +
' <md-table-cell align="center">10</md-table-cell>' +
' <md-table-cell align="center">Unlimited</md-table-cell>' +
' <md-table-cell align="center">Unlimited</md-table-cell>' +
' </md-table-row>' +
' <md-table-row value="sla">' +
' <md-table-cell [head]="true" scope="row">' +
' <md-tooltip text="Measured per calendar month; service credits apply automatically.">' +
' <span tabindex="0" class="fine-print">Uptime SLA</span>' +
' </md-tooltip>' +
' </md-table-cell>' +
' <md-table-cell align="center">—</md-table-cell>' +
' <md-table-cell align="center">99.9%</md-table-cell>' +
' <md-table-cell align="center">99.95%</md-table-cell>' +
' </md-table-row>' +
' </md-table-body>' +
' </md-table>' +
' </md-table-container>' +
' <div class="pricing-faq">' +
' <span class="faq-title">Frequently asked questions</span>' +
' <md-accordion [exclusive]="true" [headingLevel]="3" variant="outlined">' +
' <md-accordion-item headline="Can I change plans later?">' +
' <p>Yes — upgrades apply immediately and are prorated; downgrades take effect at the next renewal.</p>' +
' </md-accordion-item>' +
' <md-accordion-item headline="What happens when my trial ends?">' +
' <p>Your workspace becomes read-only. Nothing is deleted for 90 days, and picking any plan restores it instantly.</p>' +
' </md-accordion-item>' +
' </md-accordion>' +
' </div>' +
'</div>',
styles: [
'.pricing { display: grid; gap: 28px; } ' +
'.pricing-head { display: grid; justify-items: center; gap: 12px; text-align: center; } ' +
'.pricing-title { font: var(--md-sys-typescale-headline-small); } ' +
'.pricing-savings { font: var(--md-sys-typescale-body-small); color: var(--md-sys-color-on-surface-variant); } ' +
'.pricing-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; align-items: stretch; } ' +
'md-card.popular { position: relative; --md-card-outline-color: var(--md-sys-color-primary); --md-card-outline-width: 2px; } ' +
'.pop-badge { --md-badge-offset-x: -14px; --md-badge-offset-y: 14px; } ' +
'.plan-name { font: var(--md-sys-typescale-title-medium); } ' +
'.plan-price { font: var(--md-sys-typescale-display-small); } ' +
'.plan-term { font: var(--md-sys-typescale-body-small); color: var(--md-sys-color-on-surface-variant); } ' +
'.plan-chips { display: flex; flex-wrap: wrap; gap: 6px; } ' +
'.plan-features { margin: 0; padding: 0; list-style: none; display: grid; gap: 6px; } ' +
'.plan-cta { margin-top: auto; } ' +
'.pricing-faq { display: grid; gap: 12px; } ' +
'.faq-title { font: var(--md-sys-typescale-title-medium); } ' +
'.fine-print { text-decoration: underline dotted; text-underline-offset: 3px; cursor: help; }'
],
})
export class PricingPageComponent {
annual = false;
plans = [
{ name: 'Starter', monthly: 19, annual: 15, popular: false, variant: 'outlined', cta: 'Start free trial', features: ['10 projects', '3 team members', 'Community support'] },
{ name: 'Pro · Most popular', monthly: 49, annual: 39, popular: true, variant: 'filled', cta: 'Choose Pro', features: ['Unlimited projects', '25 team members'] },
{ name: 'Scale', monthly: 99, annual: 79, popular: false, variant: 'outlined', cta: 'Contact sales', features: ['Unlimited everything', '99.95% uptime SLA', 'Dedicated support'] },
];
// Single-select mdChange always reports exactly one value in e.detail[0].
onBilling(e: CustomEvent<string[]>) { this.annual = e.detail[0] === 'annual'; }
price(p: { monthly: number; annual: number }) { return '$' + (this.annual ? p.annual : p.monthly); }
get term() { return this.annual ? ' per month, billed annually' : ' per month, billed monthly'; }
get savings() { return this.annual ? 'You are saving 20% with annual billing' : 'Save 20% with annual billing'; }
}<script setup lang="ts">
import { computed, ref } from 'vue';
import {
MdAccordion, MdAccordionItem, MdBadge, MdButton, MdCard, MdChip,
MdSegmentedButton, MdSegmentedButtonSet, MdTable, MdTableBody, MdTableCell,
MdTableContainer, MdTableHead, MdTableRow, MdTooltip,
} from '@awc-ui/vue';
const annual = ref(false);
const plans = [
{ name: 'Starter', monthly: 19, annual: 15, popular: false, variant: 'outlined', cta: 'Start free trial', features: ['10 projects', '3 team members', 'Community support'] },
{ name: 'Pro · Most popular', monthly: 49, annual: 39, popular: true, variant: 'filled', cta: 'Choose Pro', features: ['Unlimited projects', '25 team members'] },
{ name: 'Scale', monthly: 99, annual: 79, popular: false, variant: 'outlined', cta: 'Contact sales', features: ['Unlimited everything', '99.95% uptime SLA', 'Dedicated support'] },
];
const term = computed(() => (annual.value ? ' per month, billed annually' : ' per month, billed monthly'));
const savings = computed(() => (annual.value ? 'You are saving 20% with annual billing' : 'Save 20% with annual billing'));
function price(p: { monthly: number; annual: number }) {
return '$' + (annual.value ? p.annual : p.monthly);
}
</script>
<template>
<div class="pricing">
<div class="pricing-head">
<span class="pricing-title">Plans that grow with your team</span>
<!-- Single-select mdChange always reports exactly one value in detail[0]. -->
<MdSegmentedButtonSet aria-label="Billing period" @mdChange="annual = $event.detail[0] === 'annual'">
<MdSegmentedButton value="monthly" label="Monthly" :selected="!annual" />
<MdSegmentedButton value="annual" label="Annual" :selected="annual" />
</MdSegmentedButtonSet>
<span class="pricing-savings">{{ savings }}</span>
</div>
<div class="pricing-grid">
<MdCard v-for="p in plans" :key="p.name" variant="outlined" :class="{ popular: p.popular }">
<MdBadge v-if="p.popular" icon="star" class="pop-badge" />
<span class="plan-name">{{ p.name }}</span>
<div><span class="plan-price">{{ price(p) }}</span><span class="plan-term">{{ term }}</span></div>
<div v-if="p.popular" role="group" aria-label="Pro plan highlights" class="plan-chips">
<MdChip variant="assist" label="SSO" :density="-2" />
<MdChip variant="assist" label="API access" :density="-2" />
<MdChip variant="assist" label="Priority support" :density="-2" />
</div>
<ul class="plan-features"><li v-for="f in p.features" :key="f">{{ f }}</li></ul>
<MdButton :variant="p.variant" :fullWidth="true" class="plan-cta">{{ p.cta }}</MdButton>
</MdCard>
</div>
<MdTableContainer variant="outlined">
<MdTable columnTemplate="1.5fr 1fr 1fr 1fr" label="Plan comparison" :striped="true">
<MdTableHead>
<MdTableRow rowgroup="head">
<MdTableCell :head="true" scope="col">Feature</MdTableCell>
<MdTableCell :head="true" scope="col" align="center">Starter</MdTableCell>
<MdTableCell :head="true" scope="col" align="center">Pro</MdTableCell>
<MdTableCell :head="true" scope="col" align="center">Scale</MdTableCell>
</MdTableRow>
</MdTableHead>
<MdTableBody>
<MdTableRow value="projects">
<MdTableCell :head="true" scope="row">Projects</MdTableCell>
<MdTableCell align="center">10</MdTableCell>
<MdTableCell align="center">Unlimited</MdTableCell>
<MdTableCell align="center">Unlimited</MdTableCell>
</MdTableRow>
<MdTableRow value="sla">
<MdTableCell :head="true" scope="row">
<MdTooltip text="Measured per calendar month; service credits apply automatically.">
<span tabindex="0" class="fine-print">Uptime SLA</span>
</MdTooltip>
</MdTableCell>
<MdTableCell align="center">—</MdTableCell>
<MdTableCell align="center">99.9%</MdTableCell>
<MdTableCell align="center">99.95%</MdTableCell>
</MdTableRow>
</MdTableBody>
</MdTable>
</MdTableContainer>
<div class="pricing-faq">
<span class="faq-title">Frequently asked questions</span>
<MdAccordion :exclusive="true" :headingLevel="3" variant="outlined">
<MdAccordionItem headline="Can I change plans later?">
<p>Yes — upgrades apply immediately and are prorated; downgrades take effect at the next renewal.</p>
</MdAccordionItem>
<MdAccordionItem headline="What happens when my trial ends?">
<p>Your workspace becomes read-only. Nothing is deleted for 90 days, and picking any plan restores it instantly.</p>
</MdAccordionItem>
</MdAccordion>
</div>
</div>
</template>
<style scoped>
.pricing { display: grid; gap: 28px; }
.pricing-head { display: grid; justify-items: center; gap: 12px; text-align: center; }
.pricing-title { font: var(--md-sys-typescale-headline-small); }
.pricing-savings { font: var(--md-sys-typescale-body-small); color: var(--md-sys-color-on-surface-variant); }
.pricing-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; align-items: stretch; }
md-card.popular { position: relative; --md-card-outline-color: var(--md-sys-color-primary); --md-card-outline-width: 2px; }
.pop-badge { --md-badge-offset-x: -14px; --md-badge-offset-y: 14px; }
.plan-name { font: var(--md-sys-typescale-title-medium); }
.plan-price { font: var(--md-sys-typescale-display-small); }
.plan-term { font: var(--md-sys-typescale-body-small); color: var(--md-sys-color-on-surface-variant); }
.plan-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.plan-features { margin: 0; padding: 0; list-style: none; display: grid; gap: 6px; }
.plan-cta { margin-top: auto; }
.pricing-faq { display: grid; gap: 12px; }
.faq-title { font: var(--md-sys-typescale-title-medium); }
.fine-print { text-decoration: underline dotted; text-underline-offset: 3px; cursor: help; }
</style><script>
import { defineCustomElements } from '@awc-ui/svelte';
// Client-only registration — in SvelteKit guard with: if (browser) defineCustomElements(window);
if (typeof window !== 'undefined') defineCustomElements(window);
let annual = false;
const plans = [
{ name: 'Starter', monthly: 19, annual: 15, popular: false, variant: 'outlined', cta: 'Start free trial', features: ['10 projects', '3 team members', 'Community support'] },
{ name: 'Pro · Most popular', monthly: 49, annual: 39, popular: true, variant: 'filled', cta: 'Choose Pro', features: ['Unlimited projects', '25 team members'] },
{ name: 'Scale', monthly: 99, annual: 79, popular: false, variant: 'outlined', cta: 'Contact sales', features: ['Unlimited everything', '99.95% uptime SLA', 'Dedicated support'] },
];
$: term = annual ? ' per month, billed annually' : ' per month, billed monthly';
$: savings = annual ? 'You are saving 20% with annual billing' : 'Save 20% with annual billing';
// Single-select mdChange always reports exactly one value in e.detail[0].
function onBilling(e) {
annual = e.detail[0] === 'annual';
}
</script>
<div class="pricing">
<div class="pricing-head">
<span class="pricing-title">Plans that grow with your team</span>
<md-segmented-button-set aria-label="Billing period" on:mdChange={onBilling}>
<md-segmented-button value="monthly" label="Monthly" selected={!annual}></md-segmented-button>
<md-segmented-button value="annual" label="Annual" selected={annual}></md-segmented-button>
</md-segmented-button-set>
<span class="pricing-savings">{savings}</span>
</div>
<div class="pricing-grid">
{#each plans as p (p.name)}
<md-card variant="outlined" class:popular={p.popular}>
{#if p.popular}<md-badge icon="star" class="pop-badge"></md-badge>{/if}
<span class="plan-name">{p.name}</span>
<div><span class="plan-price">{'$' + (annual ? p.annual : p.monthly)}</span><span class="plan-term">{term}</span></div>
{#if p.popular}
<div role="group" aria-label="Pro plan highlights" class="plan-chips">
<md-chip variant="assist" label="SSO" density="-2"></md-chip>
<md-chip variant="assist" label="API access" density="-2"></md-chip>
<md-chip variant="assist" label="Priority support" density="-2"></md-chip>
</div>
{/if}
<ul class="plan-features">{#each p.features as f}<li>{f}</li>{/each}</ul>
<md-button variant={p.variant} fullWidth={true} class="plan-cta">{p.cta}</md-button>
</md-card>
{/each}
</div>
<md-table-container variant="outlined">
<md-table columnTemplate="1.5fr 1fr 1fr 1fr" label="Plan comparison" striped>
<md-table-head>
<md-table-row rowgroup="head">
<md-table-cell head scope="col">Feature</md-table-cell>
<md-table-cell head scope="col" align="center">Starter</md-table-cell>
<md-table-cell head scope="col" align="center">Pro</md-table-cell>
<md-table-cell head scope="col" align="center">Scale</md-table-cell>
</md-table-row>
</md-table-head>
<md-table-body>
<md-table-row value="projects">
<md-table-cell head scope="row">Projects</md-table-cell>
<md-table-cell align="center">10</md-table-cell>
<md-table-cell align="center">Unlimited</md-table-cell>
<md-table-cell align="center">Unlimited</md-table-cell>
</md-table-row>
<md-table-row value="sla">
<md-table-cell head scope="row">
<md-tooltip text="Measured per calendar month; service credits apply automatically.">
<span tabindex="0" class="fine-print">Uptime SLA</span>
</md-tooltip>
</md-table-cell>
<md-table-cell align="center">—</md-table-cell>
<md-table-cell align="center">99.9%</md-table-cell>
<md-table-cell align="center">99.95%</md-table-cell>
</md-table-row>
</md-table-body>
</md-table>
</md-table-container>
<div class="pricing-faq">
<span class="faq-title">Frequently asked questions</span>
<md-accordion exclusive heading-level="3" variant="outlined">
<md-accordion-item headline="Can I change plans later?">
<p>Yes — upgrades apply immediately and are prorated; downgrades take effect at the next renewal.</p>
</md-accordion-item>
<md-accordion-item headline="What happens when my trial ends?">
<p>Your workspace becomes read-only. Nothing is deleted for 90 days, and picking any plan restores it instantly.</p>
</md-accordion-item>
</md-accordion>
</div>
</div>
<style>
.pricing { display: grid; gap: 28px; }
.pricing-head { display: grid; justify-items: center; gap: 12px; text-align: center; }
.pricing-title { font: var(--md-sys-typescale-headline-small); }
.pricing-savings { font: var(--md-sys-typescale-body-small); color: var(--md-sys-color-on-surface-variant); }
.pricing-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; align-items: stretch; }
md-card.popular { position: relative; --md-card-outline-color: var(--md-sys-color-primary); --md-card-outline-width: 2px; }
.pop-badge { --md-badge-offset-x: -14px; --md-badge-offset-y: 14px; }
.plan-name { font: var(--md-sys-typescale-title-medium); }
.plan-price { font: var(--md-sys-typescale-display-small); }
.plan-term { font: var(--md-sys-typescale-body-small); color: var(--md-sys-color-on-surface-variant); }
.plan-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.plan-features { margin: 0; padding: 0; list-style: none; display: grid; gap: 6px; }
.plan-cta { margin-top: auto; }
.pricing-faq { display: grid; gap: 12px; }
.faq-title { font: var(--md-sys-typescale-title-medium); }
.fine-print { text-decoration: underline dotted; text-underline-offset: 3px; cursor: help; }
</style>How it’s built
Section titled “How it’s built”| Component | Role in this screen |
|---|---|
md-segmented-button-set | The billing toggle. Single-select radiogroup over two segments; its mdChange carries the full selection snapshot (['annual']), and the initial state is expressed with selected on the Monthly child — the set has no value prop. |
md-card | One outlined card per plan. They hold their own md-buttons, so they are deliberately not interactive — the buttons are the controls. The popular card just overrides --md-card-outline-color and --md-card-outline-width. |
md-badge | The “popular” marker on the Pro card. It anchors to the nearest positioned ancestor, so the card gets position: relative, and the offset properties pull it inside the card’s overflow: hidden boundary. |
md-chip | Plan highlights on the Pro card, as a set inside a labelled role="group" — chips never appear alone. |
md-table | The full comparison. column-template declares the four tracks, head scope="col" / head scope="row" make every value announce with its plan and feature, and label names the grid for screen readers. |
md-tooltip | Fine print on the metered rows — the trigger is wrapped in the default slot, and each span gets tabindex="0" so keyboard users get the same disclosure. |
md-accordion | The FAQ. exclusive keeps one answer open at a time; each item renders a real heading wrapping a button with aria-expanded. |
The details that make it production-grade
Section titled “The details that make it production-grade”- The price update is idempotent by design. The set’s
mdChangefires on every activation — including re-clicking the already-selected segment — and single-select can never report an empty selection, so the handler just rewrites the DOM frome.detail[0]with no state to diff. - The badge is a glyph, not the word “Popular”.
md-badgeshows at most 4 characters ofvalue, so the card usesicon="star"plus visible text in the plan title; the badge itself isrole="status", takes no pointer events, and never blocks the card underneath. - Cards keep the keyboard path on the buttons. A card holding an
md-buttonmust not beinteractive: the component would detect the focusable child and drop its ownrole="button", leaving a mouse-only card action. Here each card is a plain container and the CTA is the single control. - Tooltip fine print is keyboard-reachable and WCAG 1.4.13 compliant. The
plain-text triggers carry
tabindex="0"(a barespangets hover only), the popup opens on focus as well as hover, and the defaulthide-delaykeeps the grace period that lets a pointer travel into the popup. The tooltip setsaria-descriptionon the trigger — a description, never a name. - The comparison table is a real ARIA grid.
md-tablelays out with CSS Grid but stampsrole="table"/row/columnheader/rowheaderfrom yourhead+scopeattributes, so “99.9%” announces as Uptime SLA, Pro rather than as loose text.
Variations
Section titled “Variations”- Localized currency: format prices with
Intl.NumberFormatbefore writingtextContent, and keep the raw amounts in thedata-monthly/data-annualattributes. - Always-open FAQ: add
keep-one-expandednext toexclusiveso exactly one answer is open at all times — the open header advertisesaria-disabled="true"instead of collapsing. - A third billing term: segmented buttons scale to five segments, so a
“2-year” option is one more
md-segmented-buttonchild plus adata-biennialprice on each card. - A very long feature matrix: wrap the table in
md-table-container max-height="60vh"and setfrozen-headerso the plan names stay visible while the body scrolls.
Related
Section titled “Related”- Recipe: Checkout wizard — where the “Choose Pro” click leads: payment, confirmation, and an OTP step.
- Recipe: Notification preferences — another settings-style surface built on cards and toggles.
- Component manuals:
md-segmented-button,md-table,md-tooltip,md-accordion.