---
/**
* PricingToggle: plans with a monthly / yearly switch. The switch is two
* native radio buttons and CSS :has(), so it needs no JavaScript and the
* monthly prices show if CSS fails.
*
* @example
* <PricingToggle title="Plans" saving="Two months free" tiers={[
* { name: "Solo", monthly: "$12", yearly: "$120", features: ["One user"], cta: { label: "Start", href: "/start" } },
* ]} />
*/
import Button from "../button/Button.astro";
import Icon from "../icon/Icon.astro";
interface Cta {
label: string;
href: string;
}
interface Tier {
name: string;
monthly: string;
yearly: string;
description?: string;
features: string[];
cta: Cta;
featured?: boolean;
}
interface Props {
title: string;
accent?: string;
intro?: string;
tiers: Tier[];
/** Label next to the yearly option, e.g. "Save 20%". */
saving?: string;
monthlyLabel?: string;
yearlyLabel?: string;
as?: "h2" | "h3";
class?: string;
}
const {
title,
accent,
intro,
tiers,
saving,
monthlyLabel = "Monthly",
yearlyLabel = "Yearly",
as: Heading = "h2",
class: className,
} = Astro.props;
const group = `vs-pricing-toggle-${Math.random().toString(36).slice(2, 8)}`;
---
<section class:list={["vs-pricing-toggle", className]}>
<div class="vs-pricing-toggle__inner">
<header class="vs-pricing-toggle__head">
<Heading class="vs-display vs-pricing-toggle__title">{title}{accent && <> <em>{accent}</em></>}</Heading>
{intro && <p class="vs-pricing-toggle__intro">{intro}</p>}
<fieldset class="vs-pricing-toggle__switch">
<legend class="vs-sr-only">Billing period</legend>
<label>
<input type="radio" name={group} value="monthly" checked />
<span>{monthlyLabel}</span>
</label>
<label>
<input type="radio" name={group} value="yearly" class="vs-pricing-toggle__yearly" />
<span>
{yearlyLabel}
{saving && <em class="vs-pricing-toggle__saving">{saving}</em>}
</span>
</label>
</fieldset>
</header>
<ul class="vs-pricing-toggle__tiers">
{
tiers.map((t) => (
<li class:list={["vs-pricing-toggle__tier", { "is-featured": t.featured }]}>
<h3 class="vs-pricing-toggle__name">{t.name}</h3>
{t.description && <p class="vs-pricing-toggle__desc">{t.description}</p>}
<p class="vs-pricing-toggle__price">
<span class="vs-display vs-pricing-toggle__m">
{t.monthly}
<small>/ month</small>
</span>
<span class="vs-display vs-pricing-toggle__y">
{t.yearly}
<small>/ year</small>
</span>
</p>
<div class="vs-pricing-toggle__action">
<Button href={t.cta.href} variant={t.featured ? "accent" : "secondary"}>{t.cta.label}</Button>
</div>
<ul class="vs-pricing-toggle__features">
{t.features.map((f) => (
<li>
<Icon name="check" size={16} strokeWidth={2.25} />
<span>{f}</span>
</li>
))}
</ul>
</li>
))
}
</ul>
</div>
</section>
<style>
.vs-pricing-toggle {
container-type: inline-size;
background: var(--vs-page);
color: var(--vs-ink);
font-family: var(--vs-font-sans);
font-stretch: var(--vs-font-stretch);
}
.vs-pricing-toggle__inner {
max-width: var(--vs-container);
margin-inline: auto;
padding: var(--vs-section-y) var(--vs-gutter);
}
.vs-pricing-toggle__head {
display: grid;
justify-items: center;
text-align: center;
}
.vs-pricing-toggle__title {
font-size: clamp(2rem, 6.5cqi, 4.25rem);
}
.vs-pricing-toggle__intro {
max-width: 32rem;
margin: 1.25rem 0 0;
font-size: 1.0625rem;
line-height: 1.55;
color: var(--vs-ink-2);
}
/* The switch */
.vs-pricing-toggle__switch {
display: inline-flex;
gap: 2px;
margin: 2rem 0 0;
padding: 3px;
border: var(--vs-border-w) solid var(--vs-line-strong);
border-radius: var(--vs-radius-pill);
background: var(--vs-field);
}
.vs-pricing-toggle__switch label {
position: relative;
cursor: pointer;
}
.vs-pricing-toggle__switch input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.vs-pricing-toggle__switch span {
display: inline-flex;
align-items: center;
gap: 8px;
height: 34px;
padding: 0 16px;
border-radius: var(--vs-radius-pill);
font-size: 0.875rem;
font-weight: 500;
color: var(--vs-ink-2);
transition: background-color var(--vs-duration-fast) ease-out, color var(--vs-duration-fast) ease-out;
}
.vs-pricing-toggle__switch input:checked + span {
background: var(--vs-surface);
color: var(--vs-ink);
box-shadow: var(--vs-shadow-btn);
}
.vs-pricing-toggle__switch input:focus-visible + span {
box-shadow: var(--vs-focus-ring);
}
.vs-pricing-toggle__saving {
padding: 2px 7px;
border-radius: var(--vs-radius-pill);
background: var(--vs-accent-tint);
color: var(--vs-accent-ink);
font-size: 0.6875rem;
font-style: normal;
font-weight: 600;
}
.vs-pricing-toggle__tiers {
display: grid;
gap: 16px;
margin: clamp(28px, 5cqi, 48px) 0 0;
padding: 0;
list-style: none;
text-align: left;
}
@container (min-width: 760px) {
.vs-pricing-toggle__tiers {
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
}
}
.vs-pricing-toggle__tier {
display: flex;
flex-direction: column;
padding: 28px;
border: var(--vs-border-w) solid var(--vs-line);
border-radius: var(--vs-radius-card);
background: var(--vs-surface);
box-shadow: var(--vs-shadow-sm);
}
.vs-pricing-toggle__tier.is-featured {
border-color: var(--vs-accent);
box-shadow: 0 0 0 1px var(--vs-accent), var(--vs-pop-shadow, var(--vs-shadow-md));
}
.vs-pricing-toggle__name {
margin: 0;
font-size: 1.125rem;
font-weight: 600;
}
.vs-pricing-toggle__desc {
margin: 8px 0 0;
font-size: 0.9375rem;
line-height: 1.5;
color: var(--vs-ink-2);
}
.vs-pricing-toggle__price {
margin: 24px 0 0;
}
.vs-pricing-toggle__price .vs-display {
font-size: clamp(2.25rem, 5cqi, 3rem);
line-height: 1;
}
.vs-pricing-toggle__price small {
font-family: var(--vs-font-sans);
font-size: 0.875rem;
font-weight: 400;
letter-spacing: 0;
text-transform: none;
color: var(--vs-ink-3);
}
.vs-pricing-toggle__y {
display: none;
}
.vs-pricing-toggle:has(.vs-pricing-toggle__yearly:checked) .vs-pricing-toggle__y {
display: inline;
}
.vs-pricing-toggle:has(.vs-pricing-toggle__yearly:checked) .vs-pricing-toggle__m {
display: none;
}
.vs-pricing-toggle__action {
margin: 20px 0 24px;
}
.vs-pricing-toggle__action :global(.vs-button) {
width: 100%;
}
.vs-pricing-toggle__features {
display: grid;
gap: 10px;
margin: 0;
padding: 20px 0 0;
list-style: none;
border-top: var(--vs-border-w) solid var(--vs-line);
font-size: 0.9375rem;
}
.vs-pricing-toggle__features li {
display: flex;
gap: 10px;
line-height: 1.4;
}
.vs-pricing-toggle__features :global(.vs-icon) {
margin-top: 2px;
color: var(--vs-accent-ink);
}
</style>