# Pricing Toggle

> Plans with a monthly or yearly switch built from native radios and CSS. No JavaScript.

- **Slug:** `pricing-toggle`
- **Kind:** section · **Category:** Pricing
- **Source:** original to Viewership UI
- **Docs page:** https://viewership.ai/ui/components/pricing-toggle
- **All files in one bundle:** https://viewership.ai/ui/components/pricing-toggle.bundle.txt
- **Structured JSON:** https://viewership.ai/ui/components/pricing-toggle.json

## Install

1. Import the foundation tokens once (download: https://viewership.ai/ui/raw/foundation.css.txt):

```astro
---
// src/layouts/Layout.astro
import "../styles/foundation.css";
---
```

2. Copy this file into `src/components/viewership/pricing-toggle/`:

   - `PricingToggle.astro` (raw: https://viewership.ai/ui/raw/pricing-toggle/PricingToggle.astro.txt)

3. Copy its registry dependencies, each into its own sibling folder under `src/components/viewership/`:

   - `button`: `Button.astro` (docs: https://viewership.ai/ui/components/button.md)
   - `icon`: `Icon.astro`, `icons.ts` (docs: https://viewership.ai/ui/components/icon.md)

npm packages: none.

## Usage

```astro
---
import PricingToggle from "../components/viewership/pricing-toggle/PricingToggle.astro";
---

<PricingToggle />
```

## Example

The gallery demo, verbatim. Use the import path from Usage instead of `./`.

```astro
---
import PricingToggle from "./PricingToggle.astro";
---

<PricingToggle
  title="Gear club"
  accent="membership."
  intro="Rentals, repairs and member pricing in one subscription. Sample prices."
  saving="2 months free"
  tiers={[
    { name: "Weekend", monthly: "$9", yearly: "$90", description: "For a few trips a year.", features: ["Two rentals a month", "10% off repairs", "Member events"], cta: { label: "Join Weekend", href: "#" } },
    {
      name: "Trail",
      monthly: "$19",
      yearly: "$190",
      description: "For people out most weekends.",
      features: ["Unlimited rentals", "Free repairs", "Early access to drops", "Member events"],
      cta: { label: "Join Trail", href: "#" },
      featured: true,
    },
    { name: "Guide", monthly: "$39", yearly: "$390", description: "For guides and instructors.", features: ["Everything in Trail", "Fleet discounts", "Loaner kit on request"], cta: { label: "Join Guide", href: "#" } },
  ]}
/>
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title` | `string` |  | Section headline. |
| `accent` | `string` |  | Closing words of the headline. |
| `intro` | `string` |  | Supporting paragraph. |
| `tiers` | `{ name: string; monthly: string; yearly: string; description?: string; features: string[]; cta: { label; href }; featured?: boolean }[]` |  | Plans with both prices. |
| `saving` | `string` |  | Badge beside the yearly option, e.g. "Save 20%". |
| `monthlyLabel` | `string` | `"Monthly"` | Label for the monthly option. |
| `yearlyLabel` | `string` | `"Yearly"` | Label for the yearly option. |
| `as` | `"h2" \| "h3"` | `"h2"` | Heading element. |

## Source

### `src/components/viewership/pricing-toggle/PricingToggle.astro`

```astro
---
/**
 * 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>
```
