blocksection
Pricing tiers
The plan comparison as a row of cards, each with an accessible price and an honest not-included state.
Also ships a motion variant: Pricing tiers (motion). The static one is the default.
Preview
Installation
pnpm dlx shadcn@latest add https://facadeui.dev/r/pricing-tiers.jsonPulls in container, pricing-tier, section, section-header, types, utils. The CLI installs them for you.
Usage
The exact source of the preview above.
import { PricingTiers } from "@registry/sections/pricing-tiers"
import { PLANS } from "./content"
export function Demo() {
return (
<PricingTiers
eyebrow="Pricing"
title="Simple, and actually free"
description="Prices carry a spoken form, and a feature that is not included says so in words."
items={PLANS}
note="All prices exclude VAT."
/>
)
}Source
What the CLI copies into your project, byte for byte.
/**
* PricingTiers — the plan comparison as a row of cards.
*
* Each tier is a `PricingTier`, which handles the details that make pricing
* genuinely hard to read aloud: a spoken form for the price, "Not included"
* said in words rather than implied by a grey cross, and the recommended flag
* as text rather than as a coloured border.
*
* The optional billing toggle is deliberately *not* built in. A monthly/yearly
* switch changes prices, which means the parent already owns that state — and a
* section that owned it would have to own the pricing data model too. Pass the
* control through `toggle` and swap the `items` you hand in.
*
* a11y: a `<ul>` of plans. The toggle slot sits between the header and the list
* so it is reached before the prices it changes, in both reading and tab order.
*
* Dependencies: react, @/lib/types, @/lib/utils,
* @/components/ui/container, @/components/ui/pricing-tier, @/components/ui/section,
* @/components/ui/section-header.
*/
import type { ElementType, ReactNode } from "react"
import type {
HeadingLevel,
LinkComponent,
ListSlotProps,
SectionBaseProps,
} from "@/lib/types"
import { cn, slugId } from "@/lib/utils"
import { Container } from "@/components/ui/container"
import { PricingTier, type PricingTierItem } from "@/components/ui/pricing-tier"
import { Section } from "@/components/ui/section"
import { SectionHeader } from "@/components/ui/section-header"
export interface PricingTiersProps extends SectionBaseProps, ListSlotProps {
items: PricingTierItem[]
title?: string
eyebrow?: string
description?: string
link?: LinkComponent
/** A billing-period switch, rendered above the plans. */
toggle?: ReactNode
/** Small print under the plans. */
note?: ReactNode
tierHeadingLevel?: HeadingLevel
align?: "start" | "center"
}
export function PricingTiers({
items,
title,
eyebrow,
description,
link,
toggle,
note,
tierHeadingLevel,
align = "center",
listAs,
itemAs,
headingLevel = 2,
as,
spacing = "md",
className,
id,
}: PricingTiersProps) {
const List = (listAs ?? "ul") as ElementType
const Item = (itemAs ?? "li") as ElementType
const titleId = title ? (id ? `${id}-title` : slugId(title)) : undefined
const tierLevel = tierHeadingLevel ?? (Math.min(headingLevel + 1, 6) as HeadingLevel)
const columns =
items.length <= 2
? "sm:grid-cols-2"
: items.length === 3
? "lg:grid-cols-3"
: "sm:grid-cols-2 xl:grid-cols-4"
return (
<Section
as={as ?? (title ? "section" : "div")}
spacing={spacing}
labelledBy={titleId}
id={id}
className={className}
>
<Container className="flex flex-col gap-12">
{title ? (
<SectionHeader
align={align}
eyebrow={eyebrow}
title={title}
description={description}
headingLevel={headingLevel}
titleId={titleId}
/>
) : null}
{toggle ? (
<div
className={cn(
"flex",
align === "center" ? "justify-center" : "justify-start",
)}
>
{toggle}
</div>
) : null}
<List className={cn("grid items-stretch gap-6", columns)}>
{items.map((item) => (
// The `li` stretches and the card fills it. Deliberately not
// `display: contents` on the `li`, which has a history of dropping
// list semantics in the accessibility tree.
<Item key={item.name} className="flex">
<PricingTier
{...item}
as="div"
link={link}
headingLevel={tierLevel}
className="w-full"
/>
</Item>
))}
</List>
{note ? (
<p
className={cn(
"text-muted-foreground text-pretty text-sm",
align === "center" && "text-center",
)}
>
{note}
</p>
) : null}
</Container>
</Section>
)
}Props
PricingTiersProps
Extends SectionBaseProps, ListSlotProps.
| Prop | Type | Default |
|---|---|---|
items*Required | PricingTierItem[] | — |
title | string | — |
eyebrow | string | — |
description | string | — |
link | LinkComponent | — |
toggleA billing-period switch, rendered above the plans. | ReactNode | — |
noteSmall print under the plans. | ReactNode | — |
tierHeadingLevel | HeadingLevel | — |
align | "start" | "center" | "center" |