Billing toggle

A monthly and yearly switch with a savings badge and prices that roll to the new amount.

inputsnew
Preview
Billing toggle preview
$190$228per year

When to use

  • The billing period switch at the top of a pricing page.
  • Any price display that should roll to a new amount when a period or plan changes, using BillingPrice.
  • A small plan picker with a savings note, such as yearly versus lifetime.

When not to use

  • Use segmented-control for switching views or filters with no pricing meaning.
  • Use radio-group when options need descriptions or a vertical layout.
  • Use switch for a single on or off setting.

shadcn CLI

Also installs (2)

  • manicat-foundation
  • animated-counter
Manual install(+)
registry.jsonjson
{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "billing-toggle",
  "type": "registry:ui",
  "title": "Billing toggle",
  "description": "A monthly and yearly switch with a savings badge and prices that roll to the new amount.",
  "categories": [
    "inputs"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://manicat.viraza.net/r/manicat-foundation.json",
    "https://manicat.viraza.net/r/animated-counter.json"
  ],
  "files": [
    {
      "path": "registry/components/billing-toggle/billing-toggle.tsx",
      "type": "registry:component",
      "target": "@components/manicat/billing-toggle/billing-toggle.tsx"
    },
    {
      "path": "registry/components/billing-toggle/billing-toggle.module.css",
      "type": "registry:component",
      "target": "@components/manicat/billing-toggle/billing-toggle.module.css"
    },
    {
      "path": "registry/components/animated-counter/animated-counter.tsx",
      "type": "registry:component",
      "target": "@components/manicat/animated-counter/animated-counter.tsx"
    },
    {
      "path": "registry/components/animated-counter/animated-counter.module.css",
      "type": "registry:component",
      "target": "@components/manicat/animated-counter/animated-counter.module.css"
    }
  ],
  "docs": "Docs and live preview: https://uiarc.dev/components/billing-toggle",
  "meta": {
    "tier": "free",
    "kind": "component",
    "docs": "https://manicat.viraza.net/components/billing-toggle",
    "markdown": "https://uiarc.dev/components/billing-toggle/markdown",
    "tags": [
      "inputs",
      "new"
    ]
  }
}

Usage

Install the component, then copy this into your app. Change the content and the props to fit your screen.

example.tsxtsx
"use client";

import { useState } from "react";
import { BillingPrice, BillingToggle } from "@/components/manicat/billing-toggle/billing-toggle";

export function BillingToggleExample() {
  const [period, setPeriod] = useState("yearly");

  return (
    <div className="grid justify-items-center gap-3">
      <BillingToggle value={period} onValueChange={setPeriod} />
      <BillingPrice amount={period === "yearly" ? 190 : 19} period={period === "yearly" ? "per year" : "per month"} was={period === "yearly" ? 228 : undefined} />
    </div>
  );
}

Examples

Real screens rather than isolated props. Switch to Code to copy a whole example into your project.

Pricing toggle The price animates when the billing period flips.

Pricing toggle The price animates when the billing period flips. preview
$190$228per year

Keyboard interactions

KeysAction
TabMoves focus to the selected option only (roving tabindex).
Arrow Right / Arrow DownSelects and focuses the next option, wrapping at the end.
Arrow Left / Arrow UpSelects and focuses the previous option, wrapping at the start.
Home / EndSelects the first or last option.

Accessibility

  • The toggle is role="radiogroup" labelled by label. Each option is a button with role="radio" and aria-checked.
  • Badges are part of the option text, so the saving is read with the option name.
  • The sliding thumb is aria-hidden.
  • BillingPrice uses del for the old price. The rolling digits come from AnimatedCounter.

Motion

  • One thumb is measured from the selected option and glides to its x and width on a critically damped spring (0.34s, no bounce).
  • When an option with a badge is selected, the badge takes a faint --billing-accent tint and its text crossfades with a short rise. Every candidate text reserves space, so the badge never changes width.
  • BillingPrice rolls digits to the new amount, fades the old price in from the left, and swaps the period text in place.
  • Reduced motion jumps the thumb and swaps badge text without travel.

Responsive behavior

  • The group is inline-flex and capped at 100% width. Options do not wrap, so keep labels and badges short on phones.
  • Hover color only applies on devices with a fine pointer. Tap highlight is removed on touch.

Performance

  • Only the thumb and badge text animate. One ResizeObserver re-measures the thumb when the group resizes.
  • BillingPrice renders one AnimatedCounter; keep it to a few per page section.

Notes for AI

  • Choose it for the monthly or yearly switch above pricing cards. Use segmented-control for general view switching that has nothing to do with price.
  • It is controlled only. Keep the period in state and derive every price from it so all cards update together.
  • Put the concrete saving in activeBadge, and a percentage in badge, so the confirmed number appears once chosen.
  • Recolor the savings note with --billing-accent on any ancestor, for example --billing-accent: var(--accent) in a wrapper's CSS.
  • Use BillingPrice for each card's amount so the digits roll together when the period flips.
  • More than two options work, such as monthly, yearly, and lifetime.