-

Package Card

PreviousNext

A pricing and credit package card for cost centers, recharge pages, and plan selection.

Installation

pnpm
pnpm dlx shadcn@latest add https://ui.zgi.ai/r/package-card.json
npm
pnpm dlx shadcn@latest add https://ui.zgi.ai/r/package-card.json
yarn
yarn dlx shadcn@latest add https://ui.zgi.ai/r/package-card.json

Component

components/ui/package-card.tsx
"use client"

import * as React from "react"
import { CheckIcon, Loader2Icon } from "lucide-react"

import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Skeleton } from "@/components/ui/skeleton"

export interface PackageCardProps
  extends Omit<React.ComponentProps<typeof Card>, "title"> {
  title: React.ReactNode
  points: React.ReactNode
  price: React.ReactNode
  description?: React.ReactNode
  badge?: React.ReactNode
  features?: React.ReactNode[]
  actionLabel?: React.ReactNode
  loading?: boolean
  busy?: boolean
  onSelect?: () => void
}

export function PackageCard({
  title,
  points,
  price,
  description,
  badge,
  features = [],
  actionLabel = "Buy now",
  loading = false,
  busy = false,
  onSelect,
  className,
  ...props
}: PackageCardProps) {
  if (loading) {
    return (
      <Card className={cn("rounded-lg py-0", className)} {...props}>
        <CardContent className="space-y-4 p-4">
          <Skeleton className="h-5 w-28" />
          <Skeleton className="h-8 w-36" />
          <Skeleton className="h-5 w-20" />
          <Skeleton className="h-9 w-full" />
        </CardContent>
      </Card>
    )
  }

  return (
    <Card
      className={cn(
        "hover:border-primary/40 relative overflow-hidden rounded-lg py-0 transition-colors",
        className
      )}
      {...props}
    >
      <CardContent className="flex min-h-56 flex-col gap-4 p-4">
        {badge ? (
          <Badge className="absolute top-3 right-3 rounded-md">{badge}</Badge>
        ) : null}
        <div className="pr-16">
          <h3 className="text-sm font-medium">{title}</h3>
          {description ? (
            <p className="text-muted-foreground mt-1 line-clamp-2 text-xs leading-5">
              {description}
            </p>
          ) : null}
        </div>
        <div>
          <div className="text-2xl font-semibold tracking-normal">{points}</div>
          <div className="text-muted-foreground mt-1 text-sm">{price}</div>
        </div>
        {features.length ? (
          <ul className="space-y-2 text-xs">
            {features.map((feature, index) => (
              <li key={index} className="flex gap-2">
                <CheckIcon className="text-primary mt-0.5 size-3.5 shrink-0" />
                <span className="text-muted-foreground">{feature}</span>
              </li>
            ))}
          </ul>
        ) : null}
        <Button
          type="button"
          className="mt-auto h-9 rounded-md"
          disabled={busy}
          onClick={onSelect}
        >
          {busy ? <Loader2Icon className="size-4 animate-spin" /> : null}
          {actionLabel}
        </Button>
      </CardContent>
    </Card>
  )
}

Usage

import { PackageCard } from "@/components/ui/package-card"
 
export function Example() {
  return (
    <PackageCard
      title="Growth"
      points="1M credits"
      price="$149"
      badge="Popular"
      features={["Priority queues", "Team analytics"]}
    />
  )
}

Props

PropTypeDefault
titleReactNoderequired
pointsReactNoderequired
priceReactNoderequired
descriptionReactNode-
badgeReactNode-
featuresReactNode[][]
actionLabelReactNode"Buy now"
loadingbooleanfalse
busybooleanfalse
onSelect() => void-