Billing toggle

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

pnpm dlx shadcn@latest add @uiarc/billing-toggle
Live · keyboard ready

Plans

  • Solo

    One person, every device

    $8$10/ mo$96 billed yearly
  • Team

    Shared spaces for up to 20

    $16$20/ mo$192 billed yearly
  • Company

    SSO, audit log, and admin roles

    $32$40/ mo$384 billed yearly
  • 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.
  • 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.

Installation

Add Billing toggle with the shadcn CLI, or copy the source by hand.

pnpm dlx shadcn@latest add @uiarc/billing-toggle

Adds the component and its local dependencies, and installs motion. First time? Add the @uiarc registry to components.json, or use the full URL:

example.tsx
import { useState } from "react";import { BillingPrice, BillingToggle } from "@/registry/components/billing-toggle/billing-toggle"; export function ProPrice() {  const [period, setPeriod] = useState("monthly");  const yearly = period === "yearly";  return (    <div>      <BillingToggle        value={period}        onValueChange={setPeriod}        options={[          { value: "monthly", label: "Monthly" },          { value: "yearly", label: "Yearly", badge: "Save 20%", activeBadge: "You save $48" },        ]}      />      <BillingPrice amount={yearly ? 16 : 20} was={yearly ? 20 : undefined} period={yearly ? "per month, billed yearly" : "per month"} />    </div>  );}

API reference

3 parts. The first is the root.

BillingToggle

A billing period switch. One thumb glides between the options, and the savings note on the cheaper period tints and rewrites itself in place once it is chosen. Set --billing-accent on any ancestor to recolor the note (defaults to --success).

PropTypeDefaultDescription
valueRequiredstring–Value of the selected option. Controlled only.
onValueChangeRequired(value: string) => void–Called with the chosen option's value on click or arrow key.
optionsBillingToggleOption[]Monthly and Yearly with a Save 20% badgeOptions in order. Each has value, label, and optional badge and activeBadge.
labelstring"Billing period"Accessible name of the radio group.
size"md" | "lg""md"md is 36px tall per option, lg is 44px with larger text.
classNamestring–Extra class on the radio group.

BillingToggleOption

One option of the toggle.

PropTypeDefaultDescription
valueRequiredstring–Value passed to onValueChange. Also the React key.
labelRequiredstring–Visible option text.
badgestring–Short savings note, such as Save 20%.
activeBadgestring–Badge text once this option is selected, such as You save $48. Defaults to badge.

BillingPrice

A price that rolls to its new amount when the billing period changes, with an optional struck-through old price and a period that swaps in place.

PropTypeDefaultDescription
amountRequirednumber–The price to show.
currencystring"$"Symbol before the amount and the old price.
periodstring–Text after the price, such as per month. Swaps with a short fade when it changes.
wasnumber–Previous price. Shown struck through only when it is higher than amount.
decimalsnumber0Decimal places for amount and was.
classNamestring–Extra class on the price.
Tab
Moves focus to the selected option only (roving tabindex).
Arrow RightorArrow Down
Selects and focuses the next option, wrapping at the end.
Arrow LeftorArrow Up
Selects and focuses the previous option, wrapping at the start.
HomeorEnd
Selects the first or last option.
  • 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.
  • 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.
  • 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.
  • 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

Give your coding assistant the Markdown reference instead of screenshots.

  • 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.

The full library index for assistants is at /llms.txt.