asafefe.dev

Billing Settings

New

Subscription settings for a SaaS dashboard: the current plan with usage meters that fill in, a monthly or yearly toggle with plans whose prices roll, the card on file, and a cancel flow with a confirm step.

motionlucide-reactutilsbadgebuttoncarditemprogressseparatorspinner@soultel/rolling-number@soultel/use-reduced-motion-safe

Current plan

Pro
Renews Nov 26, 2026

$24/mo

Billed monthly

Seats4 of 5
x
Storage18.4 of 50 GB
x
Automation runs9,420 of 10,000
x

Change plan

Visa 4242

Expires 08/28

Installation

$ pnpm dlx shadcn@latest add @soultel/billing-settings

No namespace set up? npx shadcn@latest add https://asafefe.dev/r/billing-settings.json works too.

Manual installation
  1. 1. Install the dependencies.
    terminal1 lines
    npm install motion lucide-react
  2. 2. Copy the source from the Code tab into components/billing-settings.tsx.

Usage

example.tsx24 lines
import { BillingSettings } from "@/components/billing-settings"

export function Billing({ account }: { account: Account }) {
  return (
    <BillingSettings
      plans={[
        { id: "free", name: "Free", description: "For trying things out", price: { month: 0, year: 0 } },
        { id: "pro", name: "Pro", description: "For small teams", price: { month: 24, year: 230 } },
        { id: "team", name: "Team", description: "Unlimited projects and SSO", price: { month: 59, year: 566 } },
      ]}
      defaultPlan={account.planId}
      defaultInterval={account.interval}
      renewsAt={account.renewsAt}
      usage={[
        { id: "seats", label: "Seats", used: 4, limit: 5 },
        { id: "storage", label: "Storage", used: 18.4, limit: 50, unit: "GB" },
      ]}
      paymentMethod={{ brand: "Visa", last4: "4242", expMonth: 8, expYear: 2028 }}
      onUpdatePayment={() => openBillingPortal()}
      onChangePlan={(planId, interval) => updateSubscription({ planId, interval })}
      onCancel={(reason) => cancelSubscription({ reason })}
    />
  )
}

Props

PropTypeDefaultDescription
plansBillingPlan[]-{ id, name, description, price: { month, year } }[]. month is the monthly price, year the yearly total. 0 and 0 shows Free.
plan / defaultPlanstring-Controlled or initial id of the plan the account is on.
onPlanChange(planId: string) => void-Called after a confirmed switch.
interval / defaultInterval"month" | "year""month"Controlled or initial billing interval.
onIntervalChange(interval) => void-Called after a confirmed switch.
usageUsageMeter[]-{ id, label, used, limit, unit }[]. A meter turns vermilion at 90%.
paymentMethodPaymentMethod-{ brand, last4, expMonth, expYear } for the card on file.
onUpdatePayment() => void-Shows an Update button by the card.
onChangePlan(planId, interval) => void | Promise<void>-Called when the visitor confirms a switch. A promise shows a pending state; a rejection keeps the old plan.
onCancel(reason?: string) => void | Promise<void>-Called from the confirm step with the picked reason.
onResume() => void | Promise<void>-Called by Resume plan after a cancel.
cancelReasonsstring[]-Chips offered in the cancel step. An empty array skips the question.
renewsAtstring-Next renewal as "YYYY-MM-DD".
defaultCancelledbooleanfalseStart in the cancelled state.
currencystring"$"Symbol shown before prices.