Pricing Toggle Switch
A monthly/yearly billing switch with the price sliding to its new value.
- Category
- Components
- Added
- 2026-08-23
- Deps
- 1
- Updated
- 2026-09-01
Props
"use client";
import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
export function PricingToggleSwitch({
monthlyPrice = 29,
yearlyPrice = 290,
currency = "$",
}: {
monthlyPrice?: number;
yearlyPrice?: number;
currency?: string;
}) {
const [yearly, setYearly] = useState(false);
const price = yearly ? yearlyPrice : monthlyPrice;
const savePct = Math.round((1 - yearlyPrice / (monthlyPrice * 12)) * 100);
return (
<div className="flex flex-col items-center gap-5 rounded-2xl border border-neutral-200 p-8">
<div className="flex items-center gap-3">
<span className={`text-sm font-medium ${!yearly ? "text-neutral-900" : "text-neutral-400"}`}>
Monthly
</span>
<button
role="switch"
aria-checked={yearly}
onClick={() => setYearly((v) => !v)}
className={`h-5 w-9 rounded-full transition-colors ${yearly ? "bg-red-500" : "bg-neutral-200"} relative`}
>
<span
className={`absolute top-0.5 left-0.5 h-4 w-4 rounded-full bg-white transition-transform ${
yearly ? "translate-x-4" : "translate-x-0"
}`}
/>
</button>
<span className={`text-sm font-medium ${yearly ? "text-neutral-900" : "text-neutral-400"}`}>
Yearly
{savePct > 0 && (
<span className="ml-1.5 rounded-full bg-red-50 px-1.5 py-0.5 text-[10px] font-bold text-red-600">
Save {savePct}%
</span>
)}
</span>
</div>
<div className="relative flex h-14 items-start overflow-hidden">
<AnimatePresence mode="popLayout">
<motion.div
key={yearly ? "yearly" : "monthly"}
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -20, opacity: 0 }}
transition={{ duration: 0.3, ease: [0.65, 0, 0.35, 1] }}
className="flex items-start text-4xl font-black text-neutral-900"
>
{currency}
{price}
<span className="mt-2 ml-1 text-sm font-medium text-neutral-400">/{yearly ? "yr" : "mo"}</span>
</motion.div>
</AnimatePresence>
</div>
</div>
);
}
About this component
Swapping the displayed price between monthly and yearly billing with a plain state update is instant but forgettable, especially if the two numbers happen to share a leading digit. This slides the outgoing price up and out while the new one slides up from below, using AnimatePresence in popLayout mode keyed on the billing period, the same vertical-swap pattern you would use for an odometer digit. The toggle switch itself is a real switch-role element with aria-checked wired to state, not a repurposed checkbox, which is worth keeping if you pull this into a form. The implementation detail worth copying carefully: the thumb has an explicit left-0.5 resting position rather than relying on default static positioning plus a translate-x utility when active. Skip the explicit left and the thumb's untoggled position depends on the track's own padding and sizing rather than being pinned, which looks fine in isolation and glitches the moment the track's size changes.
Curator’s note
The toggle track+thumb reuses the exact pattern fixed sitewide for the shared boolean control: an explicit left-0.5 resting position on the thumb, not an implicit static position plus translate — omitting the explicit left is what made the props-panel switch glitch outside its own track when activated.
Pairs well with
Matched on shared tags and category — the entries most likely to be used alongside this one.
Usage Pricing Calculator
componentsA live estimator with real marginal-tier overage pricing and an annual discount toggle.
Pricing Page
templatesA pricing page that computes: seat slider, annual toggle and an expandable comparison matrix.
Animated Checkbox
componentsA checkbox whose checkmark draws itself in as a real SVG path animation.