Zoeken...  ⌘K GitHub

PricingToggle Sections

Pricing met maandelijks/jaarlijks toggle. Highlighted plan, feature lijst, populair badge.

/pricing-toggle
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