Pricing cards
Five pricing layouts: a plain tier, a highlighted tier, a compact row, a billing toggle and a feature list. Every plan name, price and feature line is placeholder template content that you replace with your own.
Installation
The styled layer is free and installs like any other ai2 component.
Run the following command
npx shadcn@latest add @ai2/card-pricingDependencies, the @ai2/tokens theme and the component file are installed together.
Install dependencies
npm install lucide-reactCopy the source
components/ui/card-pricing.tsx"use client"
import * as React from "react"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
/* Pricing card family: 5 card templates showing a price and plan layout. The content is ENTIRELY placeholder: generic plan names, sample figures, filler rows like "Feature one". These cards are a template, not a real offer. Colour comes ONLY from semantic tokens, transparency through color-mix. size = the padding plus radius scale (the same as card-styled). */
export type StyledSize = "sm" | "md" | "lg" | "xl"
const radii: Record<StyledSize, string> = {
sm: "rounded-lg",
md: "rounded-xl",
lg: "rounded-2xl",
xl: "rounded-2xl",
}
const pads: Record<StyledSize, string> = {
sm: "p-4",
md: "p-5",
lg: "p-6",
xl: "p-8",
}
const priceSizes: Record<StyledSize, string> = {
sm: "text-2xl",
md: "text-3xl",
lg: "text-4xl",
xl: "text-5xl",
}
type Props = React.ComponentProps<"div"> & { size?: StyledSize }
const shell = "relative w-64 border border-border bg-card text-card-foreground"
const planName = "text-sm font-semibold text-foreground"
const planNote = "text-xs leading-relaxed text-muted-foreground"
const cta =
"mt-1 inline-flex h-9 w-full select-none items-center justify-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground outline-none transition-colors duration-(--motion-fast) ease-(--motion-ease) hover:bg-[color-mix(in_oklab,var(--color-primary)_88%,var(--color-foreground))] focus-visible:ring-[3px] focus-visible:ring-ring/50 motion-reduce:transition-none"
const ctaMuted =
"mt-1 inline-flex h-9 w-full select-none items-center justify-center rounded-lg border border-border bg-secondary px-4 text-sm font-medium text-secondary-foreground outline-none transition-colors duration-(--motion-fast) ease-(--motion-ease) hover:bg-[color-mix(in_oklab,var(--color-foreground)_6%,transparent)] focus-visible:ring-[3px] focus-visible:ring-ring/50 motion-reduce:transition-none"
/* Placeholder ozellik satirlari. Gercek bir ozellik listesi degildir. */
const featureList = ["Feature one", "Feature two", "Feature three"]
function Price({ size, amount, unit }: { size: StyledSize; amount: string; unit?: string }) {
return (
<div className="flex items-baseline gap-1">
<span className={cn("font-semibold tabular-nums text-foreground", priceSizes[size])}>
{amount}
</span>
{unit ? <span className="text-xs text-muted-foreground">{unit}</span> : null}
</div>
)
}
function Features({ items }: { items: string[] }) {
return (
<ul className="flex flex-col gap-2">
{items.map((f) => (
<li
key={f}
className="flex items-center gap-2 text-xs text-muted-foreground [&>svg]:size-3.5 [&>svg]:shrink-0 [&>svg]:text-success [&>i]:text-sm [&>i]:leading-none [&>i]:text-success"
>
<Check />
{f}
</li>
))}
</ul>
)
}
/* Tier: temel paket karti. Ad + fiyat + kisa not + eylem. */
export function TierCard({ className, size = "md", children, ...props }: Props) {
return (
<div
data-slot="styled-card"
className={cn(shell, radii[size], pads[size], className)}
{...props}
>
{children ?? (
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-1">
<span className={planName}>Plan name</span>
<span className={planNote}>A short line describing the plan.</span>
</div>
<Price size={size} amount="$00" unit="/ month" />
<button type="button" className={ctaMuted}>
Choose plan
</button>
</div>
)}
</div>
)
}
/* Featured: one cikan paket. Token ring + rozet ile isaretlenir. */
export function FeaturedCard({ className, size = "md", children, ...props }: Props) {
return (
<div
data-slot="styled-card"
className={cn(
shell,
"border-transparent ring-2 ring-primary",
radii[size],
pads[size],
className
)}
{...props}
>
<span className="absolute -top-2.5 right-4 rounded-full bg-primary px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-primary-foreground">
Highlight
</span>
{children ?? (
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-1">
<span className={planName}>Plan name</span>
<span className={planNote}>The tier a template would emphasise.</span>
</div>
<Price size={size} amount="$00" unit="/ month" />
<Features items={featureList} />
<button type="button" className={cta}>
Choose plan
</button>
</div>
)}
</div>
)
}
/* Compact: name and price on one line. For dense lists. */
export function CompactPricingCard({ className, size = "md", children, ...props }: Props) {
return (
<div
data-slot="styled-card"
className={cn(shell, radii[size], pads[size], className)}
{...props}
>
{children ?? (
<div className="flex items-center justify-between gap-4">
<div className="flex flex-col gap-0.5">
<span className={planName}>Plan name</span>
<span className={planNote}>Per seat</span>
</div>
<span className="text-lg font-semibold tabular-nums text-foreground">$00</span>
</div>
)}
</div>
)
}
/* Toggle: a monthly/yearly switcher. The state is held inside the component and
both prices are placeholders. */
export function TogglePricingCard({ className, size = "md", children, ...props }: Props) {
const [yearly, setYearly] = React.useState(false)
return (
<div
data-slot="styled-card"
className={cn(shell, radii[size], pads[size], className)}
{...props}
>
{children ?? (
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-1">
<span className={planName}>Plan name</span>
<span className={planNote}>Switch the billing period.</span>
</div>
<div
role="group"
aria-label="Billing period"
className="inline-flex w-fit rounded-lg border border-border bg-surface-2 p-0.5"
>
{(["Monthly", "Yearly"] as const).map((period) => {
const active = (period === "Yearly") === yearly
return (
<button
key={period}
type="button"
aria-pressed={active}
onClick={() => setYearly(period === "Yearly")}
className={cn(
"select-none rounded-md px-2.5 py-1 text-xs font-medium outline-none transition-colors duration-(--motion-fast) ease-(--motion-ease) focus-visible:ring-[3px] focus-visible:ring-ring/50 motion-reduce:transition-none",
active
? "bg-card text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
)}
>
{period}
</button>
)
})}
</div>
<Price size={size} amount="$00" unit={yearly ? "/ year" : "/ month"} />
</div>
)}
</div>
)
}
/* List: ozellik listesi one cikan paket karti. */
export function ListPricingCard({ className, size = "md", children, ...props }: Props) {
return (
<div
data-slot="styled-card"
className={cn(shell, radii[size], pads[size], className)}
{...props}
>
{children ?? (
<div className="flex flex-col gap-3">
<div className="flex items-baseline justify-between gap-3">
<span className={planName}>Plan name</span>
<Price size="sm" amount="$00" unit="/ month" />
</div>
<span aria-hidden="true" className="h-px w-full bg-border" />
<Features items={[...featureList, "Feature four"]} />
<button type="button" className={ctaMuted}>
Choose plan
</button>
</div>
)}
</div>
)
}Manual installs skip the @ai2/tokens theme, so add the token CSS from the theming guide or the tone colors will be missing.
Variations
5 takes on the same idea. Each is its own export, and every one accepts a size prop (sm, md, lg, xl) aligned to the base Button scale.
Tier
A plan name, a price and one action.
import { TierCard } from "@/components/ui/card-pricing"
<TierCard />Featured
A highlighted tier with a token ring and a badge.
- Feature one
- Feature two
- Feature three
import { FeaturedCard } from "@/components/ui/card-pricing"
<FeaturedCard />- Feature one
- Feature two
- Feature three
- Feature one
- Feature two
- Feature three
- Feature one
- Feature two
- Feature three
- Feature one
- Feature two
- Feature three
Compact
A single row with the name and the price for dense lists.
import { CompactPricingCard } from "@/components/ui/card-pricing"
<CompactPricingCard />Toggle
A billing period switch that swaps the price unit.
import { TogglePricingCard } from "@/components/ui/card-pricing"
<TogglePricingCard />List
A feature list under the price with an action.
- Feature one
- Feature two
- Feature three
- Feature four
import { ListPricingCard } from "@/components/ui/card-pricing"
<ListPricingCard />- Feature one
- Feature two
- Feature three
- Feature four
- Feature one
- Feature two
- Feature three
- Feature four
- Feature one
- Feature two
- Feature three
- Feature four
- Feature one
- Feature two
- Feature three
- Feature four
ai2 Pricing cards: 5 styled variations on the token system
The ai2 Pricing cards are a set of 5 decorative button variations from the styled layer of the @ai2 design system, built around pricing and tier layouts filled with placeholder template content. They are free and MIT licensed, and every color comes from a semantic token, so they theme with the rest of ai2 in light and dark.
Motion runs on framer-motion: the layouts are static, so no animation runs by default. The styled layer is opt-in, so the dependency only lands if you use it; the base components stay lean. Under reduced motion, the cards look and behave exactly the same.
What is in the ai2 Pricing cards?
5 exports in one file: Tier, Featured, Compact, Toggle and List. Each renders a native button and takes a size prop (sm, md, lg, xl) aligned to the base Button. They are separate from the base Button on purpose: the base keeps its clean variant, tone and size axes, while the styled layer carries the effects.
You own the file. Copy the one category file and you have all 5 variations, with no runtime dependency on ai2 itself.
Why use it
- On-system by construction: Every color resolves to an ai2 semantic token, so the buttons follow your theme in light and dark with no extra work.
- Effect without the sprawl: The decorations live in a dedicated styled file, so the base Button keeps its clean, predictable API.
- Accessible and honest: Each renders a real button element, keeps a visible focus ring, and respects prefers-reduced-motion.
Features
- Token-driven color: No hardcoded hex or oklch; the look recolors with your theme tokens.
- framer-motion: the layouts are static, so no animation runs by default.
- Reduced-motion aware: Under prefers-reduced-motion, the cards look and behave exactly the same.
- Size aligned to the base: Every variation takes sm, md, lg and xl matching the base Button height scale, so styled and base buttons line up in a row.
Production tips
- Use it for emphasis, not everywhere: Styled buttons draw the eye. Reserve them for the one action you want people to take on a screen, and use the base Button for the rest.
- Keep labels as verbs: The decoration adds weight, so a clear action label keeps the button scannable.
- Pick one variation per surface: The variations share a family; using two different ones in the same view competes for attention.
Works with the rest of ai2
The Pricing cards sit alongside the base Button and the rest of the @ai2 registry. They share the same token file, so a styled action next to a base button or a badge stays visually consistent in both modes.