shadimage
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 · light
iconslucide-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>
  )
}

More from paddle

Browse all components in this library →