Pricing Card
Plan name, price and period, features, and a button, with featured and current states.
The featured plan has a 2pt border in the primary color and the only solid button. The current plan’s button is disabled.
One plan per row, picked with a radio. It fits where a full card is too tall: a bottom sheet, a checkout step, a settings screen.
A segmented control switches the price between monthly and yearly billing. On yearly, the monthly price is struck through next to the discount.
The plan you push sits on the primary color, with the text and the button inverted. The other plans stay as outlined Default cards.
The plan on the left, the price and the button on the right. Made for tablets and landscape, where a stacked card wastes the width.
Large paywall tiles, App Store style, side by side in a horizontal rail. The whole tile is the radio.
The variants are separate components with their own code: install the one you need.
Installation
npx axiom add pricing-cardpnpm dlx axiom add pricing-cardyarn dlx axiom add pricing-cardbun x axiom add pricing-cardAlso copies: card, badge, button, icon, title, text and their dependencies. The file lands in components/compositions/pricing-card.tsx. It brings no tokens of its own.
Requires the check icon in your icon registry.
npx axiom add pricing-card-compactpnpm dlx axiom add pricing-card-compactyarn dlx axiom add pricing-card-compactbun x axiom add pricing-card-compactAlso copies: radio, badge, title, text and their dependencies. The file lands in components/compositions/pricing-card-compact.tsx. It brings no tokens of its own.
npx axiom add pricing-card-togglepnpm dlx axiom add pricing-card-toggleyarn dlx axiom add pricing-card-togglebun x axiom add pricing-card-toggleAlso copies: card, segmented-control, badge, button, icon, title, text and their dependencies. The file lands in components/compositions/pricing-card-toggle.tsx. It brings no tokens of its own.
Requires the check icon in your icon registry.
npx axiom add pricing-card-heropnpm dlx axiom add pricing-card-heroyarn dlx axiom add pricing-card-herobun x axiom add pricing-card-heroAlso copies: card, badge, button, icon, title, text and their dependencies. The file lands in components/compositions/pricing-card-hero.tsx. It brings no tokens of its own: the surface is primary.default and the text primary.on.
Requires the check icon in your icon registry.
npx axiom add pricing-card-horizontalpnpm dlx axiom add pricing-card-horizontalyarn dlx axiom add pricing-card-horizontalbun x axiom add pricing-card-horizontalAlso copies: card, badge, button, icon, title, text and their dependencies. The file lands in components/compositions/pricing-card-horizontal.tsx. It brings no tokens of its own.
Requires the check icon in your icon registry.
npx axiom add pricing-card-tilepnpm dlx axiom add pricing-card-tileyarn dlx axiom add pricing-card-tilebun x axiom add pricing-card-tileAlso copies: radio, badge, title, text and their dependencies. The file lands in components/compositions/pricing-card-tile.tsx. It brings no tokens of its own.
Usage
import { PricingCard } from "@/components/compositions/pricing-card";
{plans.map((plan) => (
<PricingCard
key={plan.id}
name={plan.name}
price={formatPrice(plan.price)}
period={plan.price > 0 ? "/month" : undefined}
description={plan.tagline}
features={plan.features}
badge={plan.id === "pro" ? "Most popular" : undefined}
featured={plan.id === "pro"}
current={plan.id === subscription.planId}
onAction={() => purchase(plan.id)}
/>
))}Feature one plan at most: the border and the solid button only work as a signal when they’re alone.
import { PricingCardCompact } from "@/components/compositions/pricing-card-compact";
import { RadioGroup } from "@/components/ui/radio";
<RadioGroup value={plan} onValueChange={setPlan} gap={2} accessibilityLabel="Plan">
<PricingCardCompact value="yearly" name="Yearly" price="$99" period="/year" description="$8.25 a month" badge="Save 30%" />
<PricingCardCompact value="monthly" name="Monthly" price="$12" period="/month" description="Cancel anytime" />
</RadioGroup>;Each row is a Radio: it must sit inside a RadioGroup, which holds the selected plan. Put the purchase button under the group.
import { PricingCardToggle, type Billing } from "@/components/compositions/pricing-card-toggle";
const [billing, setBilling] = useState<Billing>("yearly");
<PricingCardToggle
name="Pro"
monthlyPrice="$12"
yearlyPrice="$9.60"
discount="-20%"
yearlyCaption="$115 billed yearly"
features={["Unlimited projects", "Priority support"]}
featured
billing={billing}
onBillingChange={setBilling}
onAction={(choice) => purchase("pro", choice)}
/>;Both prices are for the same period (/month), so the struck price compares like with like. With several cards, pass the same billing to all of them: switching one switches the others.
import { PricingCard } from "@/components/compositions/pricing-card";
import { PricingCardHero } from "@/components/compositions/pricing-card-hero";
<PricingCardHero
name="Pro"
price="$12"
period="/month"
badge="Most popular"
features={["Unlimited projects", "Priority support"]}
actionLabel="Start free trial"
onAction={() => purchase("pro")}
/>
<PricingCard name="Team" price="$32" period="/month" onAction={() => purchase("team")} />;Use it once per screen. Two hero cards cancel each other out.
import { PricingCardHorizontal } from "@/components/compositions/pricing-card-horizontal";
<PricingCardHorizontal
name="Pro"
price="$12"
period="/month"
description="For people who ship every week."
features={["Unlimited projects", "Priority support"]}
featured
current={subscription.planId === "pro"}
onAction={() => purchase("pro")}
/>;The features wrap on several columns. On a phone in portrait, switch to the default card: below about 400pt, the plan column gets too narrow.
import { ScrollView } from "react-native";
import { PricingCardTile } from "@/components/compositions/pricing-card-tile";
import { RadioGroup } from "@/components/ui/radio";
<RadioGroup value={plan} onValueChange={setPlan} accessibilityLabel="Plan">
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ gap: 12, paddingHorizontal: 16, paddingEnd: 16 + width / 2 }}
>
<PricingCardTile
value="yearly"
name="Yearly"
price="$3.33"
period="/month"
description="$39.99 billed yearly"
trial="7-day free trial"
badge="Save 60%"
style={{ width: 168 }}
/>
<PricingCardTile value="monthly" name="Monthly" price="$7.99" period="/month" style={{ width: 168 }} />
</ScrollView>
</RadioGroup>;Give each tile a width. Tiles side by side share the height of the tallest one. Leave room after the last tile (half the screen width) so the rail never stops on a wall.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | — | Name of the plan. |
price | string | — | Already formatted, in large type: "$12". |
period | string | — | After the price, muted: "/month". |
description | string | — | Who the plan is for, under the price. |
features | string[] | — | Short features, each after a check. |
badge | string | — | Tag next to the name. Highlight color on the featured plan, neutral otherwise. |
featured | boolean | false | Primary border and solid button. |
current | boolean | false | The user’s plan: the button is disabled and reads currentLabel. |
actionLabel | string | "Choose plan" | Label of the button. |
currentLabel | string | "Current plan" | Label of the button on the current plan. |
onAction | () => void | — | Shows the full-width button. |
style | StyleProp<ViewStyle> | — | Extra styles for the card. |
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Given to the RadioGroup when this plan is picked. |
name | string | — | Name of the plan. |
price | string | — | Already formatted, on the right: "$99". |
period | string | — | Under the price: "/year". |
description | string | — | Under the name: "$8.25 a month". |
badge | string | — | Highlight tag next to the name: "Save 30%". |
disabled | boolean | false | Dims the row and ignores presses. |
style | StyleProp<ViewStyle> | — | Extra styles for the row. |
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | — | Name of the plan. |
monthlyPrice | string | — | Already formatted: "$12". Struck through when yearly is shown. |
yearlyPrice | string | — | Same period, billed yearly: "$9.60". |
period | string | "/month" | After both prices. |
discount | string | — | Success badge next to the yearly price: "-20%". |
yearlyCaption | string | — | Under the yearly price: "$115 billed yearly". |
description | string | — | Who the plan is for. |
features | string[] | — | Short features, each after a check. |
badge | string | — | Tag next to the name. |
featured | boolean | false | Primary border and solid button. |
billing | 'monthly' | 'yearly' | — | Controlled billing, to share one choice across several cards. |
defaultBilling | 'monthly' | 'yearly' | 'monthly' | Starting billing when uncontrolled. |
onBillingChange | (billing: Billing) => void | — | Called when the segmented control changes. |
monthlyLabel | string | "Monthly" | Label of the first segment. |
yearlyLabel | string | "Yearly" | Label of the second segment. |
actionLabel | string | "Choose plan" | Label of the button. |
onAction | (billing: Billing) => void | — | Shows the full-width button. Receives the billing shown when pressed. |
style | StyleProp<ViewStyle> | — | Extra styles for the card. |
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | — | Name of the plan. |
price | string | — | Already formatted, in display type: "$12". |
period | string | — | After the price. |
description | string | — | Who the plan is for. |
features | string[] | — | Short features, each after a check. |
badge | string | — | Highlight tag next to the name. |
actionLabel | string | "Choose plan" | Label of the button. |
onAction | () => void | — | Shows the full-width inverted button. |
style | StyleProp<ViewStyle> | — | Extra styles for the card. |
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | — | Name of the plan. |
price | string | — | Already formatted, on the right. |
period | string | — | Under the price. |
description | string | — | Who the plan is for. |
features | string[] | — | Short features, wrapping on several columns. |
badge | string | — | Tag next to the name. |
featured | boolean | false | Primary border and solid button. |
current | boolean | false | The user’s plan: the button is disabled and reads currentLabel. |
actionLabel | string | "Choose plan" | Label of the button. |
currentLabel | string | "Current plan" | Label of the button on the current plan. |
onAction | () => void | — | Shows the button under the price. |
style | StyleProp<ViewStyle> | — | Extra styles for the card. |
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Given to the RadioGroup when this plan is picked. |
name | string | — | Name of the plan. |
price | string | — | Already formatted, in large type: "$3.33". |
period | string | — | After the price: "/month". |
description | string | — | Under the price, muted: "$39.99 billed yearly". |
trial | string | — | Last line, semibold: "7-day free trial". |
badge | string | — | Highlight tag above the name: "Save 60%". |
disabled | boolean | false | Dims the tile and ignores presses. |
style | StyleProp<ViewStyle> | — | Give it a width. |
Accessibility
The price and its period are read together: “$12 /month”. Buttons are labelled with the plan: “Start free trial, Pro”, or “Current plan, Free” while disabled.
Compact and Tile are radios in a radiogroup: each is read as one item with its state, “Yearly, $99 /year, $8.25 a month, Save 30%, selected”. On Toggle, the yearly price is read with what it replaces: “$9.60 /month, instead of $12, -20%”.
Built with
- Card: the outlined surface, filled with the primary color on Hero.
- Title: the name and the price.
- Badge: the plan tag and the discount.
- Icon and Text: the features.
- Button: solid on the featured plan, outlined on the others, inverted on Hero.
- Radio: the selection of Compact and Tile.
- Segmented Control: the billing switch of Toggle.
Used in
No block uses Pricing Card yet.