Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelog
  1. Home
  2. Blog
  3. Build it: screens and apps
  4. Shadcn Pricing Page: Yearly Toggle, Table, Phone Layout
Auf Deutsch lesen
  1. Blog
  2. Build it: screens and apps
  3. Shadcn Pricing Page: Yearly Toggle, Table, Phone Layout
Build it: screens and apps
Build it: screens and apps

Shadcn Pricing Page: Yearly Toggle, Table, Phone Layout

SR
Serban Rusu · Founder of Wingo UI
Oct 16, 2026 · 10 min read

On this page

0%
  1. What does a shadcn pricing page need?
  2. How do you install the pricing table?
  3. How do you model plans and yearly prices?
  4. How does the Monthly / Yearly toggle work?
  5. How do you add the comparison table?
  6. How should plans stack on a phone?
  7. How do you connect the buttons to checkout?
  8. Do you want the whole pricing page instead?
  9. What is the next step?
  10. FAQ
    1. Does shadcn/ui have a pricing page or pricing table?
    2. Should a yearly plan show the monthly equivalent or the yearly total?
    3. How is the yearly savings percentage calculated?
    4. How do I make a pricing table responsive on mobile?
    5. Is the Wingo UI pricing table free?

TL;DR

A shadcn pricing page needs a Monthly / Yearly switch, plan cards with one featured plan, a comparison table, buttons wired to checkout and an FAQ. shadcn/ui ships no pricing component or block as of October 2026, so you compose one from Card, Tabs and Table or install a ready pricing table. With the Wingo UI Pricing Table you store each plan's yearly total, and the component shows the monthly equivalent, the savings badge, and swipeable cards with a one-column comparison on phones.

Published Oct 16, 2026

A shadcn pricing page looks simple until you build one: a Monthly / Yearly switch, three plan cards, a comparison table, buttons that start a checkout. shadcn/ui itself ships no pricing block as of October 2026, so most people start from a gallery. shadcnblocks (opens in a new tab) lists five pricing page layouts, shadcn/studio (opens in a new tab) lists twenty with one marked free, and 21st.dev hosts community pricing tables such as Kokonut UI's (opens in a new tab), which animates prices with NumberFlow (all checked on October 16, 2026). What those pages rarely spell out is the part that breaks: the yearly math, what the comparison table does on a 390px screen, and what a screen reader hears when the prices change.

This tutorial builds a React pricing page with the Pricing Table from Wingo UI. We build that library, so we are not neutral. Every snippet uses the component's real props, and every outside fact links to its source.

What does a shadcn pricing page need?

A shadcn pricing page needs five parts, in this order:

  1. A period switch. Monthly and Yearly, with the yearly saving on the Yearly option. It changes prices already on screen, so it is a radio group; tabs are for swapping panels.
  2. Plan cards. Two to four plans, one featured. Each card has a name, a one-line "who it is for", a price, a short feature list and one button.
  3. A comparison table. Every feature in rows, plans in columns. Nielsen Norman Group's comparison table guidelines (opens in a new tab) recommend about five items at most, consistent attributes for every item, and a header that stays visible while long tables scroll.
  4. Buttons wired to checkout. A button that shows it is working while your server creates the checkout session.
  5. Objection handling. An FAQ and a closing call to action. These belong to the page around the table.

If you assemble this from shadcn/ui parts, you reach for Card, Toggle Group or Tabs, and Table, then write the price formatting, the savings math, the screen reader announcement and the phone layout yourself. The rest of this post uses a React pricing table component that already does those and installs as source, like any shadcn component. It covers the first four parts in one file.

How do you install the pricing table?

One CLI command copies this shadcn pricing table and everything it imports into your project:

bash
npx wingo-ui@latest add pricing-table

The CLI copies components/ui/pricing-table.tsx into your project as source, plus the items it imports: the Segmented Control for the period switch, Price for the rolling digits, Carousel for phones, Table for the comparison, and the shared tones, motion and locale helpers. It also installs motion and lucide-react. The file is a client component ("use client"), so it works inside an App Router page.

How do you model plans and yearly prices?

Plans are plain data. A price is a number when it never changes, { monthly, yearly } when it follows the switch, 0 for free and null for a custom plan (its button defaults to "Contact us").

The one decision to get right: yearly is what the customer pays per year, the full annual amount. Store the number your billing provider charges and let the component do the division.

tsx
// app/pricing/plans.ts
import type { PricingComparisonGroup, PricingPlan } from "@/components/ui/pricing-table";
export const plans: PricingPlan[] = [
{
id: "free",
name: "Free",
description: "For side projects and trying things out.",
price: 0,
features: ["1 project", "Community support", { label: "Custom domain", included: false }],
cta: { label: "Start for free", href: "/sign-up" },
},
{
id: "starter",
name: "Starter",
description: "For freelancers who ship for clients.",
price: { monthly: 15, yearly: 144 },
featured: true,
features: [
"10 projects",
"Custom domains",
{ label: "Email support", hint: "We answer within one business day." },
],
note: "14 days free, no card",
},
{
id: "team",
name: "Team",
description: "For agencies and product teams.",
price: { monthly: 40, yearly: 384 },
featuresTitle: "Everything in Starter, plus:",
features: ["Unlimited projects", "Roles and permissions", "Audit log"],
},
];

With these numbers, Yearly shows Starter as $12/mo with "Billed $144 yearly" under it, and Team as $32/mo with "Billed $384 yearly". The Yearly option gets a "Save 20%" badge: the component computes 1 - yearly / (monthly * 12) for every plan, rounds it down and shows the best one. If you prefer the yearly amount on the card, set yearlyDisplay="per-year", and the line under it reads "You save $36" instead.

A feature can be a string (included) or { label, included?, hint? }. included: false draws it as missing, and a hint adds an (i) button that opens on hover, focus or tap.

Rendering it takes one element:

tsx
// app/pricing/page.tsx
import { PricingTable } from "@/components/ui/pricing-table";
import { plans } from "./plans";
export default function PricingRoute() {
return <PricingTable plans={plans} currency="USD" />;
}

That page can stay a server component: the plans are plain data, and PricingTable is the client boundary.

Switch Monthly to Yearly and watch every price roll to its new value while the billed line changes. Hover or tap an (i) to read its hint, and scroll to the comparison below the plans.
$ npx wingo-ui@latest add pricing-table
ProPricing table docs

How does the Monthly / Yearly toggle work?

The pricing toggle is the free Segmented Control, rendered with aria-label="Billing period". It is a radio group with one tab stop: Left and Right move and select, and on touch you can press the thumb and drag it across. We explain the control in depth, including when Tabs or a Toggle Group fit better, in our React segmented control guide.

Three details matter on a pricing page:

  • The prices roll. Each digit rolls to its new value when the period changes. With reduced motion turned on, the new value appears at once. Set animatePrice={false} to skip the roll for everyone.
  • A screen reader hears the change once. A polite live region announces the period and the featured plan's new price, for example "Yearly. Starter: $12 per month, Billed $144 yearly". It stays quiet on the first render, so the page load is not read twice.
  • The switch hides itself when no plan has a yearly price. Hide it on purpose with showToggle={false}.

The component opens on Monthly. To open on Yearly without tracking state, pass defaultPeriod="yearly". To read the period from outside (for checkout or analytics), control it with period and onPeriodChange, typed with the exported BillingPeriod. The checkout section below shows the full controlled version.

How do you add the comparison table?

Pass comparison as groups of rows. Each row has one value per plan id: true is a check, false a dash, and anything else prints as text. Add it to the same plans.ts, keyed by the plan ids from above:

tsx
// app/pricing/plans.ts (continued)
export const comparison: PricingComparisonGroup[] = [
{
label: "Projects",
rows: [
{ label: "Projects", values: { free: "1", starter: "10", team: "Unlimited" } },
{ label: "Custom domains", values: { free: false, starter: true, team: true } },
],
},
{
label: "Support",
rows: [
{ label: "Email support", values: { free: false, starter: true, team: true } },
{
label: "Response time",
hint: "Business days, from the first message.",
values: { free: "Community", starter: "1 day", team: "4 hours" },
},
],
},
];

A plan id missing from values renders a dash, the same as false, so a typo or a renamed plan id silently reads as "not included". Keep the keys in sync with plans. On a wide layout this pricing comparison table renders as a real <table> whose header row of plan names and buttons sticks while you scroll, which is the sticky header the NN/g article asks for. If your site has a fixed top bar, set the --topbar-offset CSS variable on a parent to its height so the sticky header stops under it instead of behind it.

How should plans stack on a phone?

On a phone, plans should become swipeable cards and the comparison should show one plan at a time. Stacking alone is not enough: three plan cards stacked on a phone push the comparison several screens down, and a comparison table with four columns at 390px either squeezes the labels or scrolls sideways and loses them.

The Pricing Table switches layout at 768px of its own width, measured with container queries instead of the viewport. It lays out correctly in a narrow docs column or next to a sidebar, and because it is pure CSS the server HTML already has the right layout, with no flash on hydration.

Below 768px:

  • Plans become swipeable cards that open on the featured plan, with a peek of the next card and dots to jump between them. Left and Right work with focus inside. With two plans, mobileLayout="stack" puts them one under the other instead.
  • The comparison becomes a plan picker (another Segmented Control) above one column of rows for the picked plan, ending in that plan's button. This is the same move as turning a table into cards, which we cover for data tables in responsive tables as mobile cards.
  • The switch goes full width, and on touch screens it is 44px tall.

The narrow column on the docs page shows the phone layout, so you can try both: open the Pricing Table docs on a laptop and on a phone.

How do you connect the buttons to checkout?

A plan's cta takes either an href (renders a link) or an onClick. When onClick returns a promise, that plan's button shows a spinner until it settles, and repeated taps are ignored. Here is the final page, with the period controlled and each paid plan starting a checkout on your own API route:

tsx
// app/pricing/pricing-plans.tsx
"use client";
import { useState } from "react";
import { PricingTable, type BillingPeriod, type PricingPlan } from "@/components/ui/pricing-table";
import { comparison, plans } from "./plans";
async function startCheckout(planId: string, period: BillingPeriod) {
// your route creates the checkout session on the server and returns its url
const res = await fetch("/api/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ planId, period }),
});
// a rejected promise also stops the spinner, so the button can be pressed again
if (!res.ok) throw new Error(`Checkout failed: ${res.status}`);
const { url } = (await res.json()) as { url: string };
window.location.assign(url);
}
export function PricingPlans() {
const [period, setPeriod] = useState<BillingPeriod>("yearly");
const withCheckout: PricingPlan[] = plans.map((plan) =>
plan.cta?.href
? plan
: { ...plan, cta: { label: `Choose ${plan.name}`, onClick: () => startCheckout(plan.id, period) } },
);
return (
<PricingTable
plans={withCheckout}
comparison={comparison}
period={period}
onPeriodChange={setPeriod}
currency="USD"
tone="brand"
/>
);
}
tsx
// app/pricing/page.tsx
import type { Metadata } from "next";
import { PricingPlans } from "./pricing-plans";
export const metadata: Metadata = { title: "Pricing" };
export default function PricingRoute() {
return (
<main className="mx-auto w-full max-w-6xl px-4 py-16">
<h1 className="mb-10 text-center text-4xl font-semibold tracking-tight">Pricing</h1>
<PricingPlans />
</main>
);
}

Functions cannot cross from a server component to a client component, which is why the onClick plans live in pricing-plans.tsx. If every plan only has an href, you can render PricingTable straight from the server page.

tone="brand" colors the featured plan's badge and button, and any tone other than neutral turns the feature checks green. The tone defaults to neutral, which keeps the checks in the text color, and color takes any CSS color if your brand is not one of the tones. featuredStyle picks how the featured plan stands out: raised (default), outline or inverted.

Do you want the whole pricing page instead?

If you want the full route and not only the table, install the Pricing Page block. It puts the table inside a whole page: a marketing header with a phone menu in a bottom sheet, the plans with a Monthly / Yearly switch and a USD / EUR switch, customer logos, three quotes, an FAQ accordion, a closing call to action and a footer.

bash
npx wingo-ui@latest add pricing-page
tsx
// app/(marketing)/pricing/page.tsx
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { PricingPage } from "@/components/blocks/pricing-page";
export default function PricingRoute() {
const router = useRouter();
return (
<PricingPage
linkComponent={Link}
defaultPeriod="yearly"
onSelectPlan={(planId, period, currency) =>
router.push(`/sign-up?plan=${planId}&period=${period}&currency=${currency}`)
}
/>
);
}

It ships with sample data (PRICING_PAGE_SAMPLE), so it renders finished with zero props. Pass your own plans, comparison, faqs and testimonials to replace it, and turn sections off with showLogos, showTestimonials, showFaq or showCta. Euro prices are converted from your base prices at eurRate (1.08 by default) and rounded to whole euros. The conversion runs whenever the currency is EUR, so if you pass plans already priced in euros, also set currency="EUR", currencies={["EUR"]} (a single entry hides the switch) and eurRate={1}, or your euro prices get divided a second time. design="bold" swaps the classic look for heavy display type and an inverted featured plan. Set header={false} and footer={false} when the page sits inside your own site layout.

Switch the period and the currency, then pick a plan: its button spins while onSelectPlan runs. On a phone, open the menu to get the bottom sheet.
$ npx wingo-ui@latest add pricing-page
ProPricing Page docs

This block replaces the two files from the previous section: pick one or the other for /pricing. Because this page.tsx is a client component, it cannot export metadata. If you need a page title, move the PricingPage element into its own client file and keep page.tsx a server component, as in the previous section.

For a pricing band on a landing page instead of a full route, the Pricing Section is the compact version with four layouts and a "Compare all features" link. One difference to watch: its yearly price is the monthly equivalent when paid yearly, while the Pricing Table's yearly is the yearly total.

What is the next step?

Install the table with npx wingo-ui@latest add pricing-table, paste your plans, and check it at 390px before anything else. The Pricing Table and the Pricing Page are part of Wingo UI Pro; the Segmented Control that drives the switch is free. For the other screens of a SaaS app, start from our Next.js admin dashboard tutorial or browse the rest of the Build it category.

Components in this post

  • Pricing table

    A React pricing table: plans side by side with a monthly / yearly switch whose prices roll, a featured plan and a full comparison table.

    Pro
  • Pricing Page

    A React SaaS pricing page: plans with monthly or yearly and USD or EUR switches, a comparison table, quotes, an FAQ and a closing call.

    Pro
  • Segmented Control

    A React iOS-style segmented control with a raised thumb that slides and follows your finger.

    Free

FAQ

Does shadcn/ui have a pricing page or pricing table?

Not as of October 2026. The official blocks page features dashboard, sidebar, login and signup blocks, and the component list has Card, Table, Tabs and Toggle Group but no pricing component, so pricing pages come from community galleries or from composing those parts yourself.

Should a yearly plan show the monthly equivalent or the yearly total?

Show the monthly equivalent with the yearly total under it ("$12/mo, Billed $144 yearly"), so the number compares directly with the monthly price. The Wingo UI Pricing Table does this by default and switches to the yearly amount plus what it saves with yearlyDisplay="per-year".

How is the yearly savings percentage calculated?

It is 1 minus the yearly price divided by twelve monthly payments, rounded down. $144 a year against $15 a month is 1 - 144 / 180, which is 20%. The Pricing Table takes the best saving across all plans for the badge on the Yearly option.

How do I make a pricing table responsive on mobile?

Turn the plans into a swipeable row of cards that opens on the featured plan, or stack them when there are only two, and replace the comparison table with a plan picker above one column of rows. Wide comparison tables that scroll sideways on a 390px screen lose the row labels.

Is the Wingo UI pricing table free?

The Segmented Control it uses for the period switch is free. The Pricing Table, the Pricing Section and the full Pricing Page block are part of Wingo UI Pro, and all of them install with npx wingo-ui@latest add.

  • Pricing
  • React
  • shadcn/ui
  • Next.js
  • Tailwind CSS

Share

SR

About the author

Serban Rusu

Founder of Wingo UI

Serban Rusu founded Wingo UI. He builds its component library, CLI and MCP server, and writes about React interfaces for phones and AI coding agents.

More from Serban
Keep reading

Related posts

All posts
Comparisons and alternatives

Shadcn Blocks vs Components: When to Start From Which

Shadcn blocks vs components vs page templates: what each one saves you, what it costs to customize later, and a simple rule for where to start a new screen.

SRSerban Rusu·Oct 16, 2026·11 min read
Build it: screens and apps

Nextjs SaaS Landing Page: What Each Section Must Prove

Build a nextjs saas landing page section by section: what the hero, proof, features, pricing and FAQ must prove, what to cut, and how each reads on a phone.

SRSerban Rusu·Oct 16, 2026·11 min read
Mobile UI in React

React Swipeable Tabs: Pages That Follow the Thumb

Build React swipeable tabs on native scroll snap: an indicator that tracks the finger, vertical scroll that never flips the tab, keyboard support, no old deps.

SRSerban Rusu·Oct 16, 2026·10 min read
Newsletter

Get new posts by email

New guides, tutorials and comparisons from the Wingo UI blog, sent when they are published.

No spam. Unsubscribe at any time.

WingoUI

Animated, configurable, mobile-first React components and blocks. Copy the source, make it yours, and let your coding agent build with it.

Product

  • Components
  • Templates
  • Pricing
  • AI agents
  • Theme generator

Resources

  • Docs
  • Blog
  • Changelog
  • Changelog RSS
  • llms.txt

Legal

  • Terms
  • Privacy
  • Refunds
  • License
  • Imprint

Component categories

  • Buttons & Actions
  • Inputs
  • Forms
  • Navigation
  • Overlays
  • Feedback
  • Data Display
  • Tables & Lists
  • Charts & Stats
  • Layout
  • Media
  • AI Kit
  • Text & Effects
  • Mobile
  • Commerce
  • Marketing Sections
  • Blocks
  • Hooks & Utilities

© 2026 Wingo UI

Wingo UI