Pricing Card
A single pricing plan card — one tier's box, with its name, its price, what it includes and the button that takes it. Reach for it wherever a plan has to be chosen or shown: a marketing pricing page, a plans-and-billing settings screen, an upgrade, paywall or trial-ended modal, a compare-plans table, the subscription row in an account page, a checkout or plan-picker step, a seat or usage tier selector, and the free/pro/enterprise row on a landing page. Common asks it answers: "pricing card react", "pricing table component", "shadcn pricing card", "shadcn pricing page", "shadcn pricing table", "pricing tiers component react", "subscription plan card", "plan comparison card", "free pro enterprise cards", "most popular plan highlight", "featured pricing tier ring", "recommended plan badge", "upgrade modal component", "paywall component react", "billing settings plan card", "monthly yearly price toggle card", "price with billing period", "feature list with checkmarks react", "included and excluded features list", "strikethrough feature not announced screen reader", "line-through accessibility", "check and cross feature list accessible", "tailwind pricing card", "next.js pricing page component", "stripe pricing table alternative", "build your own pricing table", "heading level prop react", "pricing table heading hierarchy", "料金プラン カード react", "価格表 コンポーネント". It takes a plan name, a big price with its billing period ($29 /mo), an optional one-line pitch, a feature list and a call-to-action, and several of them sit side by side to make the whole pricing table. Features are the part that is usually got wrong. Pass a string for an included feature or an object to mark one excluded; an excluded row is muted and struck through, and it also carries a screen-reader-only "Not included:" prefix — because text-decoration: line-through is not announced, so a purely visual strikethrough tells a sighted reader the feature is missing and tells a screen-reader user it is included. The check and minus icons are aria-hidden, since the text already says which is which. Set featured to mark the recommended tier: it draws a primary ring and a "Most popular" badge, and badge takes any other label (Best value, Current plan). headingLevel picks h2, h3 or h4 so three cards dropped into a section do not break the page outline — a pricing table is the classic place where a hardcoded h3 lands under the wrong h2. Against official shadcn/ui there is no pricing card to compare with, only pieces to assemble: card is a 1,828-character generic container with no price, plan, tier or feature vocabulary in it at all, item is a ten-part compound kit (ItemGroup, Item, ItemMedia, ItemContent, ItemTitle, ItemDescription, ItemActions, ItemHeader, ItemFooter, ItemSeparator) that additionally pulls in separator and is built for list rows, and badge and button each drag in @radix-ui/react-slot. None of the four contains a single pricing term. Within pulld it is the marketing-page counterpart to feature-card, which sells a capability rather than a tier, and it pairs with segmented-control for the monthly/yearly switch above a row of them. This is one component with lucide-react as its only dependency, theme-aware through shadcn tokens in light and dark.
registry/ui/pricing-card.tsx
import * as React from "react"
import { Check, Minus } from "lucide-react"
import { cn } from "@/lib/utils"
/**
* A single plan feature. Pass a string for an included feature, or an object to
* mark it excluded (rendered muted with a strikethrough and an accessible
* "not included" label).
*/
export type PricingFeature = string | { text: string; included?: boolean }
interface PricingCardProps
extends Omit<React.ComponentPropsWithoutRef<"div">, "title"> {
/** Plan name, e.g. "Pro". Rendered as the card heading. */
name: string
/** The price, e.g. "$29" or a custom node. Kept flexible for "Free"/"Custom". */
price: React.ReactNode
/** Billing period shown next to the price, e.g. "/mo". */
period?: string
/** Short plan description under the name. */
description?: string
/** Feature list. Strings are included; objects can be marked excluded. */
features?: PricingFeature[]
/** Call-to-action, usually a link or button rendered under the features. */
cta?: React.ReactNode
/**
* Highlight this plan: adds a primary ring and shows a badge. Use for the
* recommended tier in a pricing row.
*/
featured?: boolean
/** Badge content shown when `featured` (or whenever provided). Defaults to "Most popular". */
badge?: React.ReactNode
/** Accessible heading level for the plan name. Defaults to `h3`. */
headingLevel?: "h2" | "h3" | "h4"
}
function normalize(feature: PricingFeature): { text: string; included: boolean } {
return typeof feature === "string"
? { text: feature, included: true }
: { text: feature.text, included: feature.included !== false }
}
/**
* A pricing plan card: plan name, price with billing period, an optional
* description, a checked/unchecked feature list, and a call-to-action. Set
* `featured` to highlight the recommended tier with a ring and a badge. Compose
* several side by side for a pricing page or upsell/paywall row. shadcn/ui ships
* no pricing card. Theme-aware via shadcn tokens with dark mode.
*/
export function PricingCard({
name,
price,
period,
description,
features,
cta,
featured = false,
badge,
headingLevel = "h3",
className,
...props
}: PricingCardProps) {
const Heading = headingLevel
const badgeContent = badge ?? (featured ? "Most popular" : null)
return (
<div
className={cn(
"relative flex flex-col rounded-lg border bg-card p-6 text-card-foreground",
featured && "border-primary ring-1 ring-primary",
className
)}
{...props}
>
{badgeContent ? (
<span className="absolute -top-3 left-6 inline-flex items-center rounded-full bg-primary px-3 py-0.5 text-xs font-medium text-primary-foreground">
{badgeContent}
</span>
) : null}
<Heading className="text-lg font-semibold tracking-tight">{name}</Heading>
{description ? (
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
) : null}
<div className="mt-4 flex items-baseline gap-1">
<span className="text-3xl font-semibold tracking-tight">{price}</span>
{period ? (
<span className="text-sm text-muted-foreground">{period}</span>
) : null}
</div>
{features && features.length > 0 ? (
<ul className="mt-6 flex-1 space-y-3 text-sm">
{features.map((feature, i) => {
const { text, included } = normalize(feature)
return (
<li key={i} className="flex items-start gap-2">
{included ? (
<Check
className="mt-0.5 h-4 w-4 shrink-0 text-primary"
aria-hidden="true"
/>
) : (
<Minus
className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground"
aria-hidden="true"
/>
)}
<span className={cn(!included && "text-muted-foreground line-through")}>
{!included ? <span className="sr-only">Not included: </span> : null}
{text}
</span>
</li>
)
})}
</ul>
) : null}
{cta ? <div className="mt-6">{cta}</div> : null}
</div>
)
}
- Where it goes
- navbarpricingctafooterstatsdata-tablesettings-formcheckoutfeature-grid
- Visual style
- 3dterminal-monodark-futuristiccorporate
- Interaction
- animated-counterdragmodaldropdown
- Use cases
- saas-landingdashboardecommerce
DependenciesBundle weight · light
- icons
- lucide-react