paddle
componentpricing-select-cardssource: ok · hit
Pricing select cards
Selectable pricing cards with localized prices from Paddle, available in stacked and grid layouts. Uses RadioGroup for single-plan selection — designed for express checkout flows.
npx shadcn add "https://developer.paddle.com/r/pricing-select-cards.json"free
Where it goes
pricingdata-tablecheckout
Interaction
dropdown
Use cases
saas-landingdashboardecommerce
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lighticonslucide-react
Registry dependencies
@paddle/paddle-helperscardbadgeskeletonradio-group
Files4 · source cached 10/14/2026, 11:52:52 AM
registry/new-york/blocks/pricing-select-cards/components/pricing-select-card.tsx
export {
PricingSelectCardStacked,
type PricingSelectCardStackedProps,
} from "./pricing-select-card-stacked"
export { PricingSelectCardGrid, type PricingSelectCardGridProps } from "./pricing-select-card-grid"
export {
PricingSelectCardGroup,
type PricingSelectCardGroupProps,
} from "./pricing-select-card-group"
/**
* Plan configuration for use with PricingSelectCardGroup and ExpressCheckout.
* Maps to a single Paddle price ID — pass an array of these to describe
* the selectable plans in a checkout or pricing selector.
*/
export type PricingSelectPlan = {
priceId: string
name: string
description?: string
badge?: string
icon?: React.ReactNode
}
registry/new-york/blocks/pricing-select-cards/components/pricing-select-card-stacked.tsx
"use client"
import * as React from "react"
import { RadioGroup as RadioGroupPrimitive } from "radix-ui"
import { Card, CardHeader, CardTitle } from "@/registry/new-york/ui/card"
import { Badge } from "@/registry/new-york/ui/badge"
import { Skeleton } from "@/registry/new-york/ui/skeleton"
import { cn } from "@/lib/utils"
import type { PriceData } from "@/registry/new-york/blocks/paddle-helpers/lib/paddle-types"
export type PricingSelectCardStackedProps = {
priceId: string
name: string
priceData?: PriceData
description?: string
badge?: string
badgePosition?: "left" | "center" | "right"
icon?: React.ReactNode
isCurrent?: boolean
currentPlanLabel?: string
showInterval?: boolean
loading?: boolean
className?: string
}
export function PricingSelectCardStacked({
priceId,
name,
priceData,
description,
badge,
badgePosition = "center",
icon,
isCurrent = false,
currentPlanLabel = "Current plan",
showInterval = true,
loading = false,
className,
}: PricingSelectCardStackedProps) {
const { total, originalTotal, interval, trialPeriod } = priceData ?? {}
const showBadges = badge || isCurrent
if (loading) {
return (
<Card className={cn("relative p-6", className)}>
<Skeleton className="mb-2 h-4 w-24" />
<Skeleton className="mb-1 h-8 w-32" />
<Skeleton className="h-3 w-16" />
{icon && <Skeleton className="absolute top-6 right-6 h-12 w-12 rounded-lg" />}
</Card>
)
}
return (
<RadioGroupPrimitive.Item value={priceId} disabled={isCurrent} asChild>
<Card
className={cn(
"relative flex cursor-pointer flex-col overflow-visible rounded-lg border p-6 shadow-sm transition-all hover:shadow-md",
"data-[state=checked]:border-2 data-[state=checked]:border-primary data-[state=checked]:bg-primary/5",
isCurrent && "cursor-default border-muted bg-muted/30 hover:shadow-sm",
className
)}
>
{showBadges && (
<div
className={cn(
"absolute -top-3 z-10 flex gap-1.5",
badgePosition === "left" && "left-4",
badgePosition === "center" && "left-1/2 -translate-x-1/2",
badgePosition === "right" && "right-4"
)}
>
{badge && <Badge className="bg-primary text-primary-foreground">{badge}</Badge>}
{isCurrent && <Badge variant="secondary">{currentPlanLabel}</Badge>}
</div>
)}
<div className="flex items-start justify-between">
<CardHeader className="p-0">
<div className="text-muted-foreground mb-1 text-sm">{description || name}</div>
{originalTotal && (
<div className="text-muted-foreground text-sm line-through">{originalTotal}</div>
)}
<CardTitle className="text-3xl font-bold">{total}</CardTitle>
{showInterval && interval && (
<div className="text-muted-foreground text-sm">per {interval}</div>
)}
{trialPeriod && (
<div className="text-muted-foreground text-xs mt-1">{trialPeriod} free trial</div>
)}
</CardHeader>
{icon && <div className="h-12 w-12 shrink-0 rounded-lg">{icon}</div>}
</div>
</Card>
</RadioGroupPrimitive.Item>
)
}
registry/new-york/blocks/pricing-select-cards/components/pricing-select-card-grid.tsx
"use client"
import * as React from "react"
import { RadioGroup as RadioGroupPrimitive } from "radix-ui"
import { Card, CardContent, CardHeader, CardTitle } from "@/registry/new-york/ui/card"
import { Badge } from "@/registry/new-york/ui/badge"
import { Skeleton } from "@/registry/new-york/ui/skeleton"
import { cn } from "@/lib/utils"
import { CheckIcon } from "lucide-react"
import type { PriceData } from "@/registry/new-york/blocks/paddle-helpers/lib/paddle-types"
export type PricingSelectCardGridProps = {
priceId: string
name: string
priceData?: PriceData
description?: string
badge?: string
badgePosition?: "left" | "center" | "right"
isCurrent?: boolean
currentPlanLabel?: string
showInterval?: boolean
loading?: boolean
className?: string
}
export function PricingSelectCardGrid({
priceId,
name,
priceData,
description,
badge,
badgePosition = "center",
isCurrent = false,
currentPlanLabel = "Current plan",
showInterval = true,
loading = false,
className,
}: PricingSelectCardGridProps) {
const { total, originalTotal, interval, trialPeriod } = priceData ?? {}
const showBadges = badge || isCurrent
if (loading) {
return (
<Card className={cn("relative p-5 text-center", className)}>
<Skeleton className="mx-auto mb-2 h-5 w-20" />
<Skeleton className="mx-auto mb-6 h-8 w-24" />
<Skeleton className="mx-auto h-5 w-5 rounded-full" />
</Card>
)
}
return (
<RadioGroupPrimitive.Item value={priceId} disabled={isCurrent} asChild>
<Card
className={cn(
"relative flex cursor-pointer flex-col items-center justify-center overflow-visible rounded-lg border p-5 shadow-none transition-all hover:shadow-md",
"data-[state=checked]:border-2 data-[state=checked]:border-primary data-[state=checked]:bg-primary/5",
isCurrent && "cursor-default border-muted bg-muted/30 hover:shadow-none",
className
)}
>
{showBadges && (
<div
className={cn(
"absolute -top-3 z-10 flex gap-1.5",
badgePosition === "left" && "left-4",
badgePosition === "center" && "left-1/2 -translate-x-1/2",
badgePosition === "right" && "right-4"
)}
>
{badge && <Badge className="bg-primary text-primary-foreground">{badge}</Badge>}
{isCurrent && <Badge variant="secondary">{currentPlanLabel}</Badge>}
</div>
)}
<CardHeader className="flex-1 p-0 flex flex-col items-center justify-center text-center">
<CardTitle className="text-base font-medium">{name}</CardTitle>
{description && <div className="text-muted-foreground text-xs mt-0.5">{description}</div>}
{originalTotal && (
<div className="text-muted-foreground text-sm line-through mt-2">{originalTotal}</div>
)}
<div className="text-2xl font-bold mt-2">{total}</div>
{showInterval && interval && (
<div className="text-muted-foreground text-xs">per {interval}</div>
)}
{trialPeriod && (
<div className="text-muted-foreground text-xs mt-1">{trialPeriod} free trial</div>
)}
</CardHeader>
<CardContent className="mt-6 flex items-center justify-center p-0">
<RadioGroupPrimitive.Indicator asChild forceMount>
<div
className={cn(
"group flex aspect-square size-5 shrink-0 items-center justify-center rounded-full border transition-colors",
"border-input data-[state=checked]:border-primary data-[state=checked]:bg-primary"
)}
>
<CheckIcon className="size-4 text-primary-foreground opacity-0 group-data-[state=checked]:opacity-100 transition-opacity" />
</div>
</RadioGroupPrimitive.Indicator>
</CardContent>
</Card>
</RadioGroupPrimitive.Item>
)
}
registry/new-york/blocks/pricing-select-cards/components/pricing-select-card-group.tsx
"use client"
import * as React from "react"
import { RadioGroup } from "@/registry/new-york/ui/radio-group"
import { cn } from "@/lib/utils"
export type PricingSelectCardGroupProps = {
children: React.ReactNode
value: string
onValueChange: (value: string) => void
layout?: "stacked" | "grid"
className?: string
}
export function PricingSelectCardGroup({
children,
value,
onValueChange,
layout = "stacked",
className,
}: PricingSelectCardGroupProps) {
const layoutClasses = {
stacked: "flex flex-col gap-3",
grid: "grid grid-cols-2 gap-4",
}
return (
<RadioGroup
value={value}
onValueChange={onValueChange}
className={cn(layoutClasses[layout], className)}
>
{children}
</RadioGroup>
)
}