PricingToggle Sections
Pricing met maandelijks/jaarlijks toggle. Highlighted plan, feature lijst, populair badge.
src/components/sections/PricingToggle.astro
---
/**
* PricingToggle
* Pricing met maandelijks/jaarlijks toggle. Populaire plan gemarkeerd.
*/
interface PricingPlan {
name: string;
monthlyPrice: string | number;
yearlyPrice: string | number;
currency?: string;
description?: string;
features: string[];
ctaLabel?: string;
ctaHref?: string;
popular?: boolean;
badge?: string;
}
interface Props {
eyebrow?: string;
headline?: string;
sub?: string;
plans: PricingPlan[];
yearlyDiscount?: string;
}
const {
eyebrow,
headline,
sub,
plans = [],
yearlyDiscount = '2 maanden gratis',
} = Astro.props;
---
<section class="pt" data-component="pricing-toggle">
<div class="pt__inner">
{(eyebrow || headline || sub) && (
<div class="pt__header">
{eyebrow && <p class="pt__eyebrow">{eyebrow}</p>}
{headline && <h2 class="pt__title" set:html={headline} />}
{sub && <p class="pt__sub">{sub}</p>}
</div>
)}
<!-- Toggle -->
<div class="pt__toggle-wrap">
<span class="pt__toggle-label">Maandelijks</span>
<button class="pt__toggle" id="billing-toggle" role="switch" aria-checked="false" aria-label="Schakel naar jaarlijks">
<span class="pt__toggle-thumb"></span>
</button>
<span class="pt__toggle-label">
Jaarlijks
<span class="pt__discount-badge">{yearlyDiscount}</span>
</span>
</div>
<!-- Plans -->
<div class="pt__grid">
{plans.map(plan => (
<div class:list={['pt__plan', { 'pt__plan--popular': plan.popular }]}>
{plan.popular && plan.badge && (
<div class="pt__popular-badge">{plan.badge}</div>
)}
<div class="pt__plan-header">
<h3 class="pt__plan-name">{plan.name}</h3>
{plan.description && <p class="pt__plan-desc">{plan.description}</p>}
</div>
<div class="pt__price-wrap">
<div class="pt__price" data-monthly={plan.monthlyPrice} data-yearly={plan.yearlyPrice}>
<span class="pt__currency">{plan.currency ?? '€'}</span>
<span class="pt__amount">{plan.monthlyPrice}</span>
</div>
<span class="pt__period">/ maand</span>
</div>
<a href={plan.ctaHref ?? '#'} class:list={['pt__cta', { 'pt__cta--primary': plan.popular }]}>
{plan.ctaLabel ?? 'Aan de slag'}
</a>
<ul class="pt__features">
{plan.features.map(f => (
<li class="pt__feature">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="8" fill="currentColor" opacity="0.1"/>
<path d="M5 8.5l2 2 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
{f}
</li>
))}
</ul>
</div>
))}
</div>
</div>
</section>
<style>
.pt {
background: var(--color-bg, #fff);
padding: 5rem 1.5rem;
}
.pt__inner {
max-width: 1100px;
margin: 0 auto;
}
.pt__header {
text-align: center;
margin-bottom: 2.5rem;
}
.pt__eyebrow {
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--color-accent, #6366f1);
margin-bottom: 0.75rem;
}
.pt__title {
font-size: clamp(1.875rem, 3vw, 2.75rem);
font-weight: 800;
letter-spacing: -0.03em;
color: var(--color-primary, #0a0a0a);
margin-bottom: 0.875rem;
}
.pt__title :global(em) {
font-style: normal;
color: var(--color-accent, #6366f1);
}
.pt__sub {
font-size: 1.0625rem;
color: var(--color-muted, #6b7280);
line-height: 1.6;
}
/* Toggle */
.pt__toggle-wrap {
display: flex;
align-items: center;
justify-content: center;
gap: 0.875rem;
margin-bottom: 3rem;
}
.pt__toggle-label {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9375rem;
font-weight: 500;
color: var(--color-primary, #0a0a0a);
}
.pt__toggle {
width: 48px;
height: 26px;
background: rgba(0,0,0,0.12);
border: none;
border-radius: 100px;
cursor: pointer;
position: relative;
transition: background 0.25s;
}
.pt__toggle[aria-checked="true"] {
background: var(--color-accent, #6366f1);
}
.pt__toggle-thumb {
position: absolute;
top: 3px;
left: 3px;
width: 20px;
height: 20px;
background: #fff;
border-radius: 50%;
transition: left 0.25s;
box-shadow: 0 1px 3px rgba(0,0,0,0.15);
}
.pt__toggle[aria-checked="true"] .pt__toggle-thumb {
left: calc(100% - 23px);
}
.pt__discount-badge {
background: color-mix(in srgb, var(--color-accent, #6366f1) 12%, transparent);
color: var(--color-accent, #6366f1);
font-size: 0.75rem;
font-weight: 700;
padding: 0.2rem 0.6rem;
border-radius: 100px;
}
/* Grid */
.pt__grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.25rem;
align-items: start;
}
.pt__plan {
border: 1.5px solid rgba(0,0,0,0.08);
border-radius: calc(var(--radius, 0.5rem) * 2);
padding: 2rem;
position: relative;
background: #fff;
transition: box-shadow 0.2s;
}
.pt__plan:hover {
box-shadow: 0 8px 32px rgba(0,0,0,0.07);
}
.pt__plan--popular {
border-color: var(--color-accent, #6366f1);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-accent, #6366f1) 15%, transparent);
}
.pt__popular-badge {
position: absolute;
top: -0.75rem;
left: 50%;
transform: translateX(-50%);
background: var(--color-accent, #6366f1);
color: #fff;
font-size: 0.75rem;
font-weight: 700;
padding: 0.3rem 1rem;
border-radius: 100px;
white-space: nowrap;
}
.pt__plan-name {
font-size: 1.125rem;
font-weight: 700;
color: var(--color-primary, #0a0a0a);
margin-bottom: 0.375rem;
}
.pt__plan-desc {
font-size: 0.875rem;
color: var(--color-muted, #6b7280);
margin-bottom: 1.5rem;
}
.pt__price-wrap {
display: flex;
align-items: flex-end;
gap: 0.25rem;
margin-bottom: 1.5rem;
}
.pt__price {
display: flex;
align-items: flex-start;
gap: 0.125rem;
}
.pt__currency {
font-size: 1.25rem;
font-weight: 700;
color: var(--color-primary, #0a0a0a);
margin-top: 0.5rem;
}
.pt__amount {
font-size: 3rem;
font-weight: 800;
letter-spacing: -0.04em;
color: var(--color-primary, #0a0a0a);
line-height: 1;
}
.pt__period {
font-size: 0.875rem;
color: var(--color-muted, #6b7280);
padding-bottom: 0.25rem;
}
.pt__cta {
display: block;
text-align: center;
padding: 0.8125rem;
border-radius: var(--radius, 0.5rem);
font-weight: 700;
font-size: 0.9375rem;
text-decoration: none;
margin-bottom: 1.75rem;
border: 1.5px solid rgba(0,0,0,0.12);
color: var(--color-primary, #0a0a0a);
transition: all 0.2s;
}
.pt__cta:hover { border-color: var(--color-primary, #0a0a0a); }
.pt__cta--primary {
background: var(--color-accent, #6366f1);
color: #fff;
border-color: transparent;
}
.pt__cta--primary:hover {
filter: brightness(1.1);
border-color: transparent;
}
.pt__features {
list-style: none;
display: flex;
flex-direction: column;
gap: 0.625rem;
border-top: 1px solid rgba(0,0,0,0.07);
padding-top: 1.25rem;
}
.pt__feature {
display: flex;
align-items: flex-start;
gap: 0.625rem;
font-size: 0.9rem;
color: var(--color-primary, #0a0a0a);
}
.pt__feature svg {
flex-shrink: 0;
margin-top: 1px;
color: var(--color-accent, #6366f1);
}
@media (max-width: 640px) {
.pt__grid { grid-template-columns: 1fr; }
}
</style>
<script>
const toggle = document.getElementById('billing-toggle') as HTMLButtonElement;
const amounts = document.querySelectorAll('.pt__amount');
const prices = document.querySelectorAll<HTMLElement>('.pt__price');
let isYearly = false;
toggle?.addEventListener('click', () => {
isYearly = !isYearly;
toggle.setAttribute('aria-checked', isYearly ? 'true' : 'false');
prices.forEach(priceEl => {
const monthly = priceEl.dataset.monthly ?? '';
const yearly = priceEl.dataset.yearly ?? '';
const amountEl = priceEl.querySelector('.pt__amount');
if (amountEl) {
amountEl.textContent = isYearly ? String(yearly) : String(monthly);
}
});
});
</script>
Props
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
plans * | PricingPlan[] | — | Pricing plannen met monthly en yearly prices |
yearlyDiscount | string | — | Badge bij jaarlijks label |
headline | string | — | Sectie headline |
* = verplicht