WebberUI

Pricing Table

A pricing table with a monthly/yearly segmented switch; prices spring-tween on switch, the selected pill slides, the featured plan scales up and is highlighted, and yearly shows a discount badge.

Loading preview…
npx shadcn@latest add https://webberui.com/r/pricing-table.json

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

<PricingTable />

Installation

npx shadcn@latest add https://webberui.com/r/pricing-table.json

Or, once registries are configured in components.json, install it as @webberui/pricing-table.

Usage

import { PricingTable, type PricingPlan } from "@/components/ui/pricing-table";

const plans: PricingPlan[] = [
  {
    name: "Free",
    monthly: 0,
    yearly: 0,
    features: ["1 user", "Basic analytics", "Community support"],
    cta: "Start for free",
  },
  {
    name: "Pro",
    monthly: 12,
    yearly: 115,
    features: ["5 users", "Advanced analytics", "Email support"],
    highlighted: true,
    cta: "Upgrade to Pro",
  },
  {
    name: "Team",
    monthly: 32,
    yearly: 300,
    features: ["Unlimited users", "Full analytics", "Priority support"],
    cta: "Contact us",
  },
];

<PricingTable plans={plans} currency="$" />;

The billing period switch is managed by the component internally — just fill monthly and yearly with the actual amount for each period. The discount badge is computed automatically from how much yearly saves relative to "monthly × 12", and only appears in yearly mode when there is a saving. The highlighted plan scales up, gets a highlighted border, and carries a badge.

Props

PricingTable

PropTypeDefaultDescription
plansPricingPlan[]The plans, shown side by side
currencystring"$"Currency symbol, placed before the price
classNamestringClass appended to the outermost container

PricingPlan

FieldTypeDescription
namestringPlan name
monthlynumberMonthly price (amount per month)
yearlynumberYearly price (amount per year), used to compute the discount
featuresstring[]Feature list; each row is prefixed with a Check icon
highlightedbooleanMark as the featured plan: the card scales up, the border is highlighted, and a badge is shown (optional)
ctastringCTA button text; defaults to 「開始使用」 (Get started) when not specified (optional)

How it works

  • Period switch: monthly/yearly is a segmented control whose selected pill spring-slides between the two positions using Motion's layoutId shared layout, rather than fading out and repainting
  • Price spring tween: when the period changes, each card's price is written straight into the DOM through a MotionValue (without triggering a React re-render) and springs to the new value, displayed with thousands separators and rounded to a whole number
  • Discount badge: in yearly mode, if the yearly price is lower than "monthly × 12", a 「年繳省 N%」 (Save N% yearly) badge fades in under the price via AnimatePresence; the badge sits in a fixed-height slot so its appearance and disappearance never push the layout around
  • Featured plan: at sm and above, the highlighted card scales up (scale-[1.03]), gets a thicker highlighted border, is raised in z-index, and shows a 「推薦」 (Recommended) badge at the top, with the CTA turned into a solid button
  • Equal-height side by side: the cards sit side by side in a grid and stack into a single column automatically below sm

Accessibility

  • When the user has "reduce motion" enabled at the system level, the switch pill drops layoutId (it simply appears at the new position), prices show their final value directly, and the discount badge does not fade in or out (the first render matches SSR, and the static behaviour only kicks in after mount)
  • The switch is a role="group" with an aria-label, and the two options mark the current period with aria-pressed
  • The price block carries an aria-label (with currency, amount, and period), and the rolling digits mid-tween are hidden from assistive technology (aria-hidden), so screen readers announce only the final value
  • The Check icons prefixing the features are decorative and marked aria-hidden; the features are presented as a semantic <ul> list

On this page