Billing toggle
A monthly and yearly switch with a savings badge and prices that roll to the new amount.
inputsnew
Preview
Billing toggle preview
Live · keyboard ready
$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
Live · keyboard ready
$190$228per year
pricing-toggle-the-price-animates-when-the-billing-period-flips-.tsxtsx
"use client";
import { useState } from "react";
import { BillingPrice, BillingToggle } from "@/components/manicat/billing-toggle/billing-toggle";
export function BillingTogglePricingExample() {
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>
);
}
Keyboard interactions
| Keys | Action |
|---|---|
| Tab | Moves focus to the selected option only (roving tabindex). |
| Arrow Right / Arrow Down | Selects and focuses the next option, wrapping at the end. |
| Arrow Left / Arrow Up | Selects and focuses the previous option, wrapping at the start. |
| Home / End | Selects 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.