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.
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
| Prop | Type | Default | Description |
|---|---|---|---|
plans | PricingSectionPlan[] | — | The plans, shown side by side |
eyebrow | ReactNode | — | A small label above the header |
title | ReactNode | "選擇適合你的方案" (Choose the plan that fits you) | The section's main heading |
description | ReactNode | — | The section's subheading description |
faqs | PricingFaqItem[] | — | The FAQ list; when provided, the accordion is shown |
faqTitle | ReactNode | "常見問題" (Frequently asked questions) | Heading for the FAQ block |
currency | string | "$" | Currency symbol, placed before the price |
defaultCycle | "monthly" | "yearly" | "monthly" | Initial billing period |
faqAllowMultiple | boolean | false | Whether multiple FAQ items may be expanded at once |
once | boolean | true | Play the reveal animation only the first time the element enters the viewport |
onSelectPlan | (plan) => void | — | Fires when a plan's CTA is clicked, returning that plan |
className | string | — | Class appended to the outermost <section> |
PricingSectionPlan
| Field | Type | Description |
|---|---|---|
name | string | Plan name |
description | string | A short description of the plan (optional) |
monthly | number | Monthly price (amount per month) |
yearly | number | Yearly price (amount per year), used to compute the discount |
features | string[] | Feature list; each row is prefixed with a Check icon |
highlighted | boolean | Mark as the featured plan: the card scales up, is highlighted, and shows a badge (optional) |
badge | string | Text for the featured badge; defaults to 「最受歡迎」 (Most popular) (optional) |
cta | string | CTA button text; defaults to 「開始使用」 (Get started) when not specified (optional) |
PricingFaqItem
| Field | Type | Description |
|---|---|---|
question | string | The question |
answer | ReactNode | The 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
layoutIdshared 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: autothroughAnimatePresence, with the arrow rotating in step; single-open by default (expanding a new item collapses the previous one), switchable to multi-open withfaqAllowMultiple
Accessibility
- When the user has "reduce motion" enabled at the system level, the reveal animation, the pill's
layoutIdslide, 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 arole="group"with anaria-label, and the options mark the current period witharia-pressed - The price block carries an
aria-label(with currency, amount, and period), and the rolling digits mid-tween arearia-hidden, so screen readers announce only the final value - Each FAQ item is an
aria-expanded/aria-controlsbutton, and the expanded content is arole="region"bound back to the question witharia-labelledby; the feature and arrow icons are allaria-hidden
Auth Pages
A sign-in / sign-up page pair with form motion — fields expanding and collapsing, heading and button text morphing, and a submit loading state.
Team + Logo Wall
A team member grid that staggers in, paired with an infinitely scrolling customer logo marquee — a good fit for the trust block on an about page or a home page.