Pure TailwindMIT licensed

Tailwind Monthly Yearly Billing Toggle Pricing

A zero-JS billing switch: two hidden radios, a sliding gradient thumb and every price in the section swapping at once. The CSS version needs four :has() blocks written far from the elements; the Tailwind version needs peer-checked/y: for the switch's own chrome and one group-has-[#tps04-y:checked]: pair on each price — and the swap rule now sits on the numbers that swap.

Live Demo
Plain CSSTailwind v4
Try it

Complete Tailwind CSS landing page templates in HTML, React, Next.js and Vue · from $49

Cruip · Affiliate link

Browse Cruip templates

The code

<section class="tps-04 group w-full min-h-svh bg-tps04-bg font-tps04-sans text-tps04-ink px-[clamp(20px,4vw,40px)] py-[clamp(28px,5vw,56px)]" aria-label="Pricing">
  <header class="text-center mb-[clamp(24px,4vw,40px)]">
    <h2 class="m-0 mb-2 text-[clamp(22px,3.4vw,34px)] font-bold tracking-[-0.02em] text-transparent bg-linear-to-r from-tps04-acc to-tps04-acc2 bg-clip-text forced-colors:text-[CanvasText] text-pretty">Pay how it suits you.</h2>
    <p class="m-0 mb-5.5 text-sm text-tps04-mut">Switch billing &mdash; no contracts, no negotiation.</p>
    <fieldset class="relative inline-grid grid-cols-2 gap-0 m-0 p-1 rounded-full bg-tps04-card border border-tps04-edge">
      <legend class="sr-only">Billing cycle</legend>
      <input type="radio" name="tps04-cycle" id="tps04-m" class="peer/m sr-only" checked>
      <label for="tps04-m" class="relative z-1 inline-flex items-center justify-center gap-1.5 px-5.5 py-2 rounded-full text-[13px] font-semibold text-tps04-mut whitespace-nowrap cursor-pointer transition-colors duration-200 peer-checked/m:text-tps04-bg peer-focus-visible/m:outline-2 peer-focus-visible/m:outline-tps04-acc2 peer-focus-visible/m:outline-offset-3 motion-reduce:transition-none">Monthly</label>
      <input type="radio" name="tps04-cycle" id="tps04-y" class="peer/y sr-only">
      <label for="tps04-y" class="relative z-1 inline-flex items-center justify-center gap-1.5 px-5.5 py-2 rounded-full text-[13px] font-semibold text-tps04-mut whitespace-nowrap cursor-pointer transition-colors duration-200 peer-checked/y:text-tps04-bg peer-focus-visible/y:outline-2 peer-focus-visible/y:outline-tps04-acc2 peer-focus-visible/y:outline-offset-3 motion-reduce:transition-none">Yearly <span class="px-1.5 py-0.5 rounded-md bg-tps04-chip text-tps04-acc2 text-[10px] font-bold group-has-[#tps04-y:checked]:bg-tps04-badgeon group-has-[#tps04-y:checked]:text-tps04-bg">&minus;20%</span></label>
      <span class="absolute top-1 left-1 z-0 w-[calc(50%-4px)] h-[calc(100%-8px)] rounded-full bg-linear-[135deg,var(--color-tps04-acc),var(--color-tps04-acc2)] shadow-[0_4px_14px_--theme(--color-tps04-glow)] transition-transform duration-300 ease-tps04 peer-checked/y:translate-x-full motion-reduce:transition-none" aria-hidden="true"></span>
    </fieldset>
  </header>
  <div class="grid grid-cols-1 md:grid-cols-[repeat(3,minmax(0,1fr))] gap-4 max-w-[380px] md:max-w-[980px] mx-auto">
    <article class="relative flex flex-col p-5.5 pt-6 rounded-2xl bg-tps04-card border border-tps04-edge">
      <h3 class="m-0 mb-3.5 text-[15px] font-bold uppercase tracking-[0.06em] text-tps04-acc2">Hobby</h3>
      <div class="flex items-baseline gap-2 mb-4.5">
        <span class="text-[38px] font-extrabold tracking-[-0.02em] text-tps04-price group-has-[#tps04-y:checked]:hidden">$0</span><span class="hidden text-[38px] font-extrabold tracking-[-0.02em] text-tps04-price group-has-[#tps04-y:checked]:inline">$0</span><span class="text-[13px] text-tps04-mut">forever</span>
      </div>
      <ul class="flex-1 list-none p-0 m-0 mb-5.5 text-[13.5px]/[2] text-tps04-body">
        <li><span class="inline-block w-5 font-bold text-tps04-acc2" aria-hidden="true">&#10003;</span>1 site</li>
        <li><span class="inline-block w-5 font-bold text-tps04-acc2" aria-hidden="true">&#10003;</span>Public projects</li>
        <li><span class="inline-block w-5 font-bold text-tps04-acc2" aria-hidden="true">&#10003;</span>Community</li>
      </ul>
      <a href="#hobby" class="block text-center px-4 py-2.75 rounded-lg text-[13.5px] font-semibold no-underline bg-tps04-chip text-tps04-acc2 border border-tps04-ctaedge transition-[background-color,color] duration-200 hover:bg-tps04-acc2 hover:text-tps04-bg focus-visible:outline-2 focus-visible:outline-tps04-acc2 focus-visible:outline-offset-3 motion-reduce:transition-none">Start free</a>
    </article>
    <article class="relative flex flex-col p-5.5 pt-6 rounded-2xl bg-tps04-card border border-tps04-acc2 shadow-[inset_0_0_0_1px_--theme(--color-tps04-acc2)]">
      <span class="absolute -top-2.5 right-4 px-2.5 py-1 rounded-md bg-tps04-acc2 text-tps04-bg text-[10px] font-bold uppercase tracking-[0.08em]">Best value</span>
      <h3 class="m-0 mb-3.5 text-[15px] font-bold uppercase tracking-[0.06em] text-tps04-acc2">Studio</h3>
      <div class="flex items-baseline gap-2 mb-4.5">
        <span class="text-[38px] font-extrabold tracking-[-0.02em] text-tps04-price group-has-[#tps04-y:checked]:hidden">$19</span><span class="hidden text-[38px] font-extrabold tracking-[-0.02em] text-tps04-price group-has-[#tps04-y:checked]:inline">$15</span><span class="text-[13px] text-tps04-mut">/ month</span>
      </div>
      <ul class="flex-1 list-none p-0 m-0 mb-5.5 text-[13.5px]/[2] text-tps04-body">
        <li><span class="inline-block w-5 font-bold text-tps04-acc2" aria-hidden="true">&#10003;</span>Unlimited sites</li>
        <li><span class="inline-block w-5 font-bold text-tps04-acc2" aria-hidden="true">&#10003;</span>Custom domains</li>
        <li><span class="inline-block w-5 font-bold text-tps04-acc2" aria-hidden="true">&#10003;</span>Email support</li>
        <li><span class="inline-block w-5 font-bold text-tps04-acc2" aria-hidden="true">&#10003;</span>Form submissions</li>
      </ul>
      <a href="#studio" class="block text-center px-4 py-2.75 rounded-lg text-[13.5px] font-semibold no-underline bg-tps04-acc2 text-tps04-bg border border-tps04-acc2 transition-[background-color,color] duration-200 hover:bg-tps04-acc hover:text-tps04-bg focus-visible:outline-2 focus-visible:outline-tps04-acc2 focus-visible:outline-offset-3 motion-reduce:transition-none">Start trial</a>
    </article>
    <article class="relative flex flex-col p-5.5 pt-6 rounded-2xl bg-tps04-card border border-tps04-edge">
      <h3 class="m-0 mb-3.5 text-[15px] font-bold uppercase tracking-[0.06em] text-tps04-acc2">Agency</h3>
      <div class="flex items-baseline gap-2 mb-4.5">
        <span class="text-[38px] font-extrabold tracking-[-0.02em] text-tps04-price group-has-[#tps04-y:checked]:hidden">$59</span><span class="hidden text-[38px] font-extrabold tracking-[-0.02em] text-tps04-price group-has-[#tps04-y:checked]:inline">$47</span><span class="text-[13px] text-tps04-mut">/ month</span>
      </div>
      <ul class="flex-1 list-none p-0 m-0 mb-5.5 text-[13.5px]/[2] text-tps04-body">
        <li><span class="inline-block w-5 font-bold text-tps04-acc2" aria-hidden="true">&#10003;</span>Everything in Studio</li>
        <li><span class="inline-block w-5 font-bold text-tps04-acc2" aria-hidden="true">&#10003;</span>Client billing</li>
        <li><span class="inline-block w-5 font-bold text-tps04-acc2" aria-hidden="true">&#10003;</span>Workspace switcher</li>
        <li><span class="inline-block w-5 font-bold text-tps04-acc2" aria-hidden="true">&#10003;</span>Priority support</li>
      </ul>
      <a href="#agency" class="block text-center px-4 py-2.75 rounded-lg text-[13.5px] font-semibold no-underline bg-tps04-chip text-tps04-acc2 border border-tps04-ctaedge transition-[background-color,color] duration-200 hover:bg-tps04-acc2 hover:text-tps04-bg focus-visible:outline-2 focus-visible:outline-tps04-acc2 focus-visible:outline-offset-3 motion-reduce:transition-none">Choose Agency</a>
    </article>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tps04-bg: #062021;
  --color-tps04-bg: oklch(0.2 0.03 190);
  --color-tps04-card: #0d2f30;
  --color-tps04-card: oklch(0.27 0.035 190);
  --color-tps04-edge: #1d4a4b;
  --color-tps04-edge: oklch(0.38 0.04 190);
  --color-tps04-ctaedge: #2a6a68;
  --color-tps04-ctaedge: oklch(0.48 0.06 185);
  --color-tps04-chip: #10403f;
  --color-tps04-chip: oklch(0.33 0.05 187);
  --color-tps04-badgeon: #a7f3e4;
  --color-tps04-badgeon: oklch(0.92 0.08 175);
  --color-tps04-ink: #e0fafa;
  --color-tps04-ink: oklch(0.96 0.02 190);
  --color-tps04-body: #c8eaea;
  --color-tps04-body: oklch(0.91 0.03 190);
  --color-tps04-price: #f0fff7;
  --color-tps04-price: oklch(0.98 0.02 170);
  --color-tps04-mut: #7ea7a4;
  --color-tps04-mut: oklch(0.7 0.04 185);
  --color-tps04-acc: #14b8a6;
  --color-tps04-acc: oklch(0.7 0.13 180);
  --color-tps04-acc2: #5eead4;
  --color-tps04-acc2: oklch(0.87 0.12 175);
  --color-tps04-glow: #14b8a64d;
  --color-tps04-glow: oklch(0.7 0.13 180 / 0.3);
  --font-tps04-sans: 'Inter', system-ui, sans-serif;
  --ease-tps04: cubic-bezier(0.4, 0, 0.2, 1);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
   w-full min-h-svh, so the section is correctly sized in the moment before the
   browser build has compiled its utilities. */

.tps-04 {
  width: 100%;
  min-height: 100svh;
}

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-tps04-bg: #062021;
  --color-tps04-bg: oklch(0.2 0.03 190);
  --color-tps04-card: #0d2f30;
  --color-tps04-card: oklch(0.27 0.035 190);
  --color-tps04-edge: #1d4a4b;
  --color-tps04-edge: oklch(0.38 0.04 190);
  --color-tps04-ctaedge: #2a6a68;
  --color-tps04-ctaedge: oklch(0.48 0.06 185);
  --color-tps04-chip: #10403f;
  --color-tps04-chip: oklch(0.33 0.05 187);
  --color-tps04-badgeon: #a7f3e4;
  --color-tps04-badgeon: oklch(0.92 0.08 175);
  --color-tps04-ink: #e0fafa;
  --color-tps04-ink: oklch(0.96 0.02 190);
  --color-tps04-body: #c8eaea;
  --color-tps04-body: oklch(0.91 0.03 190);
  --color-tps04-price: #f0fff7;
  --color-tps04-price: oklch(0.98 0.02 170);
  --color-tps04-mut: #7ea7a4;
  --color-tps04-mut: oklch(0.7 0.04 185);
  --color-tps04-acc: #14b8a6;
  --color-tps04-acc: oklch(0.7 0.13 180);
  --color-tps04-acc2: #5eead4;
  --color-tps04-acc2: oklch(0.87 0.12 175);
  --color-tps04-glow: #14b8a64d;
  --color-tps04-glow: oklch(0.7 0.13 180 / 0.3);
  --font-tps04-sans: 'Inter', system-ui, sans-serif;
  --ease-tps04: cubic-bezier(0.4, 0, 0.2, 1);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
   w-full min-h-svh, so the section is correctly sized in the moment before the
   browser build has compiled its utilities. */

.tps-04 {
  width: 100%;
  min-height: 100svh;
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • Four :has() blocks become two named-peer variants (thumb travel, label ink) and one arbitrary group variant repeated on the price spans. The state now reads from the elements it changes.
  • Named peers (peer/m, peer/y) are the port's key move: with one unnamed peer both labels would respond to either radio.
  • group-has-[#tps04-y:checked]: is the arbitrary-variant escape hatch, used because the boolean group-has-checked: cannot tell two radios in a group apart.
  • The hand-written .ps-04__sr clip-rect block is now just sr-only — one of the few places where a utility is strictly better than the CSS it replaces.
  • The thumb's gradient and glow read @theme tokens inside arbitrary values (bg-linear-[135deg,var(…)], shadow-[0_4px_14px_--theme(…)]), so geometry stays on the element and colour stays a token.
  • prefers-reduced-motion is three motion-reduce:transition-none utilities instead of a media block, and the switch still works with the travel removed.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Monthly Yearly Billing Toggle Pricing
Source: https://codefronts.com/layouts/tailwind-css-pricing-sections/tailwind-monthly-yearly-billing-toggle-pricing/

A zero-JS billing switch: two hidden radios, a sliding gradient thumb and every price in the section swapping at once. The CSS version needs four :has() blocks written far from the elements; the Tailwind version needs peer-checked/y: for the switch's own chrome and one group-has-[#tps04-y:checked]: pair on each price &mdash; and the swap rule now sits on the numbers that swap.
## HTML
```html
<section class="tps-04 group w-full min-h-svh bg-tps04-bg font-tps04-sans text-tps04-ink px-[clamp(20px,4vw,40px)] py-[clamp(28px,5vw,56px)]" aria-label="Pricing">
  <header class="text-center mb-[clamp(24px,4vw,40px)]">
    <h2 class="m-0 mb-2 text-[clamp(22px,3.4vw,34px)] font-bold tracking-[-0.02em] text-transparent bg-linear-to-r from-tps04-acc to-tps04-acc2 bg-clip-text forced-colors:text-[CanvasText] text-pretty">Pay how it suits you.</h2>
    <p class="m-0 mb-5.5 text-sm text-tps04-mut">Switch billing &mdash; no contracts, no negotiation.</p>
    <fieldset class="relative inline-grid grid-cols-2 gap-0 m-0 p-1 rounded-full bg-tps04-card border border-tps04-edge">
      <legend class="sr-only">Billing cycle</legend>
      <input type="radio" name="tps04-cycle" id="tps04-m" class="peer/m sr-only" checked>
      <label for="tps04-m" class="relative z-1 inline-flex items-center justify-center gap-1.5 px-5.5 py-2 rounded-full text-[13px] font-semibold text-tps04-mut whitespace-nowrap cursor-pointer transition-colors duration-200 peer-checked/m:text-tps04-bg peer-focus-visible/m:outline-2 peer-focus-visible/m:outline-tps04-acc2 peer-focus-visible/m:outline-offset-3 motion-reduce:transition-none">Monthly</label>
      <input type="radio" name="tps04-cycle" id="tps04-y" class="peer/y sr-only">
      <label for="tps04-y" class="relative z-1 inline-flex items-center justify-center gap-1.5 px-5.5 py-2 rounded-full text-[13px] font-semibold text-tps04-mut whitespace-nowrap cursor-pointer transition-colors duration-200 peer-checked/y:text-tps04-bg peer-focus-visible/y:outline-2 peer-focus-visible/y:outline-tps04-acc2 peer-focus-visible/y:outline-offset-3 motion-reduce:transition-none">Yearly <span class="px-1.5 py-0.5 rounded-md bg-tps04-chip text-tps04-acc2 text-[10px] font-bold group-has-[#tps04-y:checked]:bg-tps04-badgeon group-has-[#tps04-y:checked]:text-tps04-bg">&minus;20%</span></label>
      <span class="absolute top-1 left-1 z-0 w-[calc(50%-4px)] h-[calc(100%-8px)] rounded-full bg-linear-[135deg,var(--color-tps04-acc),var(--color-tps04-acc2)] shadow-[0_4px_14px_--theme(--color-tps04-glow)] transition-transform duration-300 ease-tps04 peer-checked/y:translate-x-full motion-reduce:transition-none" aria-hidden="true"></span>
    </fieldset>
  </header>
  <div class="grid grid-cols-1 md:grid-cols-[repeat(3,minmax(0,1fr))] gap-4 max-w-[380px] md:max-w-[980px] mx-auto">
    <article class="relative flex flex-col p-5.5 pt-6 rounded-2xl bg-tps04-card border border-tps04-edge">
      <h3 class="m-0 mb-3.5 text-[15px] font-bold uppercase tracking-[0.06em] text-tps04-acc2">Hobby</h3>
      <div class="flex items-baseline gap-2 mb-4.5">
        <span class="text-[38px] font-extrabold tracking-[-0.02em] text-tps04-price group-has-[#tps04-y:checked]:hidden">$0</span><span class="hidden text-[38px] font-extrabold tracking-[-0.02em] text-tps04-price group-has-[#tps04-y:checked]:inline">$0</span><span class="text-[13px] text-tps04-mut">forever</span>
      </div>
      <ul class="flex-1 list-none p-0 m-0 mb-5.5 text-[13.5px]/[2] text-tps04-body">
        <li><span class="inline-block w-5 font-bold text-tps04-acc2" aria-hidden="true">&#10003;</span>1 site</li>
        <li><span class="inline-block w-5 font-bold text-tps04-acc2" aria-hidden="true">&#10003;</span>Public projects</li>
        <li><span class="inline-block w-5 font-bold text-tps04-acc2" aria-hidden="true">&#10003;</span>Community</li>
      </ul>
      <a href="#hobby" class="block text-center px-4 py-2.75 rounded-lg text-[13.5px] font-semibold no-underline bg-tps04-chip text-tps04-acc2 border border-tps04-ctaedge transition-[background-color,color] duration-200 hover:bg-tps04-acc2 hover:text-tps04-bg focus-visible:outline-2 focus-visible:outline-tps04-acc2 focus-visible:outline-offset-3 motion-reduce:transition-none">Start free</a>
    </article>
    <article class="relative flex flex-col p-5.5 pt-6 rounded-2xl bg-tps04-card border border-tps04-acc2 shadow-[inset_0_0_0_1px_--theme(--color-tps04-acc2)]">
      <span class="absolute -top-2.5 right-4 px-2.5 py-1 rounded-md bg-tps04-acc2 text-tps04-bg text-[10px] font-bold uppercase tracking-[0.08em]">Best value</span>
      <h3 class="m-0 mb-3.5 text-[15px] font-bold uppercase tracking-[0.06em] text-tps04-acc2">Studio</h3>
      <div class="flex items-baseline gap-2 mb-4.5">
        <span class="text-[38px] font-extrabold tracking-[-0.02em] text-tps04-price group-has-[#tps04-y:checked]:hidden">$19</span><span class="hidden text-[38px] font-extrabold tracking-[-0.02em] text-tps04-price group-has-[#tps04-y:checked]:inline">$15</span><span class="text-[13px] text-tps04-mut">/ month</span>
      </div>
      <ul class="flex-1 list-none p-0 m-0 mb-5.5 text-[13.5px]/[2] text-tps04-body">
        <li><span class="inline-block w-5 font-bold text-tps04-acc2" aria-hidden="true">&#10003;</span>Unlimited sites</li>
        <li><span class="inline-block w-5 font-bold text-tps04-acc2" aria-hidden="true">&#10003;</span>Custom domains</li>
        <li><span class="inline-block w-5 font-bold text-tps04-acc2" aria-hidden="true">&#10003;</span>Email support</li>
        <li><span class="inline-block w-5 font-bold text-tps04-acc2" aria-hidden="true">&#10003;</span>Form submissions</li>
      </ul>
      <a href="#studio" class="block text-center px-4 py-2.75 rounded-lg text-[13.5px] font-semibold no-underline bg-tps04-acc2 text-tps04-bg border border-tps04-acc2 transition-[background-color,color] duration-200 hover:bg-tps04-acc hover:text-tps04-bg focus-visible:outline-2 focus-visible:outline-tps04-acc2 focus-visible:outline-offset-3 motion-reduce:transition-none">Start trial</a>
    </article>
    <article class="relative flex flex-col p-5.5 pt-6 rounded-2xl bg-tps04-card border border-tps04-edge">
      <h3 class="m-0 mb-3.5 text-[15px] font-bold uppercase tracking-[0.06em] text-tps04-acc2">Agency</h3>
      <div class="flex items-baseline gap-2 mb-4.5">
        <span class="text-[38px] font-extrabold tracking-[-0.02em] text-tps04-price group-has-[#tps04-y:checked]:hidden">$59</span><span class="hidden text-[38px] font-extrabold tracking-[-0.02em] text-tps04-price group-has-[#tps04-y:checked]:inline">$47</span><span class="text-[13px] text-tps04-mut">/ month</span>
      </div>
      <ul class="flex-1 list-none p-0 m-0 mb-5.5 text-[13.5px]/[2] text-tps04-body">
        <li><span class="inline-block w-5 font-bold text-tps04-acc2" aria-hidden="true">&#10003;</span>Everything in Studio</li>
        <li><span class="inline-block w-5 font-bold text-tps04-acc2" aria-hidden="true">&#10003;</span>Client billing</li>
        <li><span class="inline-block w-5 font-bold text-tps04-acc2" aria-hidden="true">&#10003;</span>Workspace switcher</li>
        <li><span class="inline-block w-5 font-bold text-tps04-acc2" aria-hidden="true">&#10003;</span>Priority support</li>
      </ul>
      <a href="#agency" class="block text-center px-4 py-2.75 rounded-lg text-[13.5px] font-semibold no-underline bg-tps04-chip text-tps04-acc2 border border-tps04-ctaedge transition-[background-color,color] duration-200 hover:bg-tps04-acc2 hover:text-tps04-bg focus-visible:outline-2 focus-visible:outline-tps04-acc2 focus-visible:outline-offset-3 motion-reduce:transition-none">Choose Agency</a>
    </article>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tps04-bg: #062021;
  --color-tps04-bg: oklch(0.2 0.03 190);
  --color-tps04-card: #0d2f30;
  --color-tps04-card: oklch(0.27 0.035 190);
  --color-tps04-edge: #1d4a4b;
  --color-tps04-edge: oklch(0.38 0.04 190);
  --color-tps04-ctaedge: #2a6a68;
  --color-tps04-ctaedge: oklch(0.48 0.06 185);
  --color-tps04-chip: #10403f;
  --color-tps04-chip: oklch(0.33 0.05 187);
  --color-tps04-badgeon: #a7f3e4;
  --color-tps04-badgeon: oklch(0.92 0.08 175);
  --color-tps04-ink: #e0fafa;
  --color-tps04-ink: oklch(0.96 0.02 190);
  --color-tps04-body: #c8eaea;
  --color-tps04-body: oklch(0.91 0.03 190);
  --color-tps04-price: #f0fff7;
  --color-tps04-price: oklch(0.98 0.02 170);
  --color-tps04-mut: #7ea7a4;
  --color-tps04-mut: oklch(0.7 0.04 185);
  --color-tps04-acc: #14b8a6;
  --color-tps04-acc: oklch(0.7 0.13 180);
  --color-tps04-acc2: #5eead4;
  --color-tps04-acc2: oklch(0.87 0.12 175);
  --color-tps04-glow: #14b8a64d;
  --color-tps04-glow: oklch(0.7 0.13 180 / 0.3);
  --font-tps04-sans: 'Inter', system-ui, sans-serif;
  --ease-tps04: cubic-bezier(0.4, 0, 0.2, 1);
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
   w-full min-h-svh, so the section is correctly sized in the moment before the
   browser build has compiled its utilities. */

.tps-04 {
  width: 100%;
  min-height: 100svh;
}
```

How this works

The switch is three siblings and two variants.

<fieldset class="relative inline-grid grid-cols-2 p-1 rounded-full …">
  <legend class="sr-only">Billing cycle</legend>
  <input id="tps04-m" type="radio" class="peer/m sr-only" checked>
  <label class="z-1 peer-checked/m:text-tps04-bg …">Monthly</label>
  <input id="tps04-y" type="radio" class="peer/y sr-only">
  <label class="z-1 peer-checked/y:text-tps04-bg …">Yearly</label>
  <span class="absolute … peer-checked/y:translate-x-full"></span>
</fieldset>

Named peers are what make this work with two radios in one group: peer-checked: alone cannot say which sibling is checked.

The price swap has to travel further. Peer variants only see siblings, and the prices are three cards away, so the section carries group and each price span reads the radio by id:

<span class="group-has-[#tps04-y:checked]:hidden">$19</span>
<span class="hidden group-has-[#tps04-y:checked]:inline">$15</span>

This is the arbitrary-variant escape hatch used exactly as intended: group-has-checked: is boolean and cannot distinguish the two radios, so the selector names the one that matters.

Why radios, not a checkbox. Two labelled radios in a <fieldset> with a <legend> announce as a two-option group with the current choice named. A styled checkbox announces “checked” and leaves the user to infer what that means about billing.

Make it yours

  • Change the discount: edit the yearly <span> numbers and the &minus;20% pill text — the swap mechanism has no arithmetic in it. Real per-seat maths is demo 11.
  • Add a third cycle (monthly / yearly / biennial): add a radio+label pair, change the fieldset to grid-cols-3, and make the thumb w-[calc(33.333%-4px)] with peer-checked/b:translate-x-[200%].
  • Thumb style: it is a gradient pill; rounded-lg plus a flat bg-tps04-acc2 reads more utilitarian, and shadow-none removes the lift.
  • Retheme through --color-tps04-acc/--color-tps04-acc2 — thumb, headline gradient, badges, checks, borders and focus rings all read them.
  • Default to yearly by moving checked to the second radio and swapping the two hidden utilities; nothing else changes.

Gotchas — read before shipping

  • sr-only, not hidden, on the radios. display:none removes them from the tab order and the switch becomes keyboard-dead.
  • The thumb must come AFTER both radios in the DOM — peer-* compiles to a sibling combinator and cannot look backwards.
  • Named peers are mandatory here. With plain peer-checked: both labels light up whenever either radio is checked.
  • The arbitrary variant needs the real id: group-has-[#tps04-y:checked]:. Rename the input and the prices stop swapping, with no error anywhere.
  • Two prices in the DOM means screen readers can read both. Keep the swap as hidden (which sets display:none), never opacity-0, or the hidden number is still announced.
  • :has() is the load-bearing selector. Below Safari 15.4 / Firefox 121 the thumb parks at Monthly and prices never swap — acceptable degradation, but decide it deliberately.
  • Both labels need z-1 (or higher) to sit above the absolutely positioned thumb, otherwise the gradient covers the active label's text.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

The whole demo rests on :has() (Chrome 105, Safari 15.4, Firefox 121) — which is what group-has-* compiles to. Without it the switch renders but does not switch.

Techniques used in this demo

Theme tokensset in @theme, used by a utility

Search CodeFronts

Loading…