WebberUI

Pricing Section

A complete pricing section — header, monthly/yearly switch, spring-tweened prices, and a featured plan — with a built-in expandable FAQ accordion, all revealing with a stagger in one pass.

This is a WebberUI Pro component

Free during the launch campaign: sign up or sign in, then hit “Copy install command” in the preview above and it installs straight away — no payment, no credit card. The command below returns 401 while you are signed out.

How to install Pro components →See the plans →

Loading preview…
npx shadcn@latest add "https://webberui.com/r/pricing-section.json?t=<install token>"

Playground

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

<PricingSection />

Installation

npx shadcn@latest add "https://webberui.com/r/pricing-section.json?t=<install token>"

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

Usage

import {
  PricingSection,
  type PricingSectionPlan,
  type PricingFaqItem,
} from "@/components/ui/pricing-section";

const plans: PricingSectionPlan[] = [
  {
    name: "Starter",
    description: "For personal side projects",
    monthly: 0,
    yearly: 0,
    features: ["3 projects", "Community support", "1GB of storage"],
    cta: "Start for free",
  },
  {
    name: "Pro",
    description: "For growing teams",
    monthly: 24,
    yearly: 230,
    features: ["Unlimited projects", "Priority support", "50GB of storage", "Advanced analytics"],
    highlighted: true,
  },
  {
    name: "Enterprise",
    monthly: 60,
    yearly: 600,
    features: ["Dedicated account manager", "SSO / SAML", "Unlimited storage"],
    cta: "Contact us",
  },
];

const faqs: PricingFaqItem[] = [
  { question: "Can I change plans at any time?", answer: "Of course — upgrades take effect immediately." },
  { question: "Is there a discount for paying yearly?", answer: "Switching to yearly saves you up to about 20%." },
];

<PricingSection
  eyebrow="Pricing"
  title="Simple, transparent pricing"
  description="Upgrade flexibly as your team grows, and cancel whenever you like."
  plans={plans}
  faqs={faqs}
  currency="$"
  onSelectPlan={(plan) => console.log(plan.name)}
/>;

The billing period switch is managed by the component internally — just fill monthly and yearly with the actual amount for each period, and the discount badge is computed automatically from how much yearly saves relative to "monthly × 12". faqs is optional; when provided, an accordion is shown below the plans, and when it is not, the whole FAQ block is not rendered at all.

Props

PricingSection

PropTypeDefaultDescription
plansPricingSectionPlan[]The plans, shown side by side
eyebrowReactNodeA small label above the header
titleReactNode"選擇適合你的方案" (Choose the plan that fits you)The section's main heading
descriptionReactNodeThe section's subheading description
faqsPricingFaqItem[]The FAQ list; when provided, the accordion is shown
faqTitleReactNode"常見問題" (Frequently asked questions)Heading for the FAQ block
currencystring"$"Currency symbol, placed before the price
defaultCycle"monthly" | "yearly""monthly"Initial billing period
faqAllowMultiplebooleanfalseWhether multiple FAQ items may be expanded at once
oncebooleantruePlay the reveal animation only the first time the element enters the viewport
onSelectPlan(plan) => voidFires when a plan's CTA is clicked, returning that plan
classNamestringClass appended to the outermost <section>

PricingSectionPlan

FieldTypeDescription
namestringPlan name
descriptionstringA short description of the plan (optional)
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, is highlighted, and shows a badge (optional)
badgestringText for the featured badge; defaults to 「最受歡迎」 (Most popular) (optional)
ctastringCTA button text; defaults to 「開始使用」 (Get started) when not specified (optional)

PricingFaqItem

FieldTypeDescription
questionstringThe question
answerReactNodeThe answer, either plain text or any node

How it works

  • Staggered reveal: the plan cards and the FAQ block are triggered with whileInView, and the cards fade in and float up in order, playing only when they enter the viewport
  • Period switch: monthly/yearly is a segmented control whose selected pill spring-slides between the two positions using Motion's layoutId shared layout
  • Price spring tween: when the period changes, prices are written straight into the DOM through a MotionValue (without triggering a React re-render) and spring to the new value, with thousands separators and rounded to a whole number
  • Discount badge: in yearly mode, if the yearly price is lower than "monthly × 12", 「年繳省 N%」 (Save N% yearly) fades in below the price via AnimatePresence, placed in a fixed-height slot so it does not push the layout around
  • FAQ accordion: expands and collapses by tweening height: auto through AnimatePresence, with the arrow rotating in step; single-open by default (expanding a new item collapses the previous one), switchable to multi-open with faqAllowMultiple

Accessibility

  • When the user has "reduce motion" enabled at the system level, the reveal animation, the pill's layoutId slide, the price tween, the discount fade in, and the FAQ expansion are all disabled and their final state is presented directly (the first render matches SSR, and the static behaviour only kicks in after mount)
  • The whole section is bound to its main heading with <section aria-labelledby>; the switch is a role="group" with an aria-label, and the 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 aria-hidden, so screen readers announce only the final value
  • Each FAQ item is an aria-expanded / aria-controls button, and the expanded content is a role="region" bound back to the question with aria-labelledby; the feature and arrow icons are all aria-hidden

On this page