Skip to content
6

Billing Card

Current-plan summary with price, a usage meter, billing detail rows and footer actions. Composable parts for billing settings.

Preview

Installation

npx shadcn@latest add https://hirael.com/r/billing-card.json

Code

components/blocks/billing-card.tsx
'use client';

import * as React from 'react';

import { cn } from '@/lib/utils';
import { Button } from '@/registry/hirael/bases/radix/ui/button';

type BillingCardProps = React.ComponentProps<'div'>;

const BillingCard = ({ className, ...props }: BillingCardProps) => {
  return (
    <div
      data-slot="billing-card"
      className={cn('flex flex-col gap-4 rounded-lg border border-border bg-card p-5 text-card-foreground', className)}
      {...props}
    />
  );
};

type BillingCardHeaderProps = React.ComponentProps<'div'>;

const BillingCardHeader = ({ className, ...props }: BillingCardHeaderProps) => {
  return (
    <div
      data-slot="billing-card-header"
      className={cn('flex items-start justify-between gap-3', className)}
      {...props}
    />
  );
};

type BillingCardEyebrowProps = React.ComponentProps<'p'>;

const BillingCardEyebrow = ({ className, ...props }: BillingCardEyebrowProps) => {
  return (
    <p
      data-slot="billing-card-eyebrow"
      className={cn('font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground', className)}
      {...props}
    />
  );
};

type BillingCardPlanProps = React.ComponentProps<'p'>;

const BillingCardPlan = ({ className, ...props }: BillingCardPlanProps) => {
  return (
    <p
      data-slot="billing-card-plan"
      className={cn('text-xl font-semibold tracking-tight text-foreground', className)}
      {...props}
    />
  );
};

interface BillingCardPriceProps extends React.ComponentProps<'p'> {
  cycle?: React.ReactNode;
}

const BillingCardPrice = ({ cycle, className, children, ...props }: BillingCardPriceProps) => {
  return (
    <p data-slot="billing-card-price" className={cn('text-end text-sm text-foreground', className)} {...props}>
      <span className="text-lg font-semibold tracking-tight">{children}</span>
      {cycle ? <span className="text-muted-foreground"> / {cycle}</span> : null}
    </p>
  );
};

interface BillingCardMeterProps extends React.ComponentProps<'div'> {
  value: number;
  max: number;
  label?: React.ReactNode;
}

const BillingCardMeter = ({ value, max, label, className, ...props }: BillingCardMeterProps) => {
  const pct = Math.max(0, Math.min(100, max ? (value / max) * 100 : 0));
  return (
    <div data-slot="billing-card-meter" className={cn('flex flex-col gap-1.5', className)} {...props}>
      {label ? (
        <div className="flex items-center justify-between text-xs">
          <span className="text-muted-foreground">{label}</span>
          <span className="font-mono tabular-nums text-foreground">
            {value} / {max}
          </span>
        </div>
      ) : null}
      <div
        role="progressbar"
        aria-valuenow={value}
        aria-valuemin={0}
        aria-valuemax={max}
        className="h-1.5 w-full overflow-hidden rounded-full bg-muted"
      >
        <div
          className="h-full rounded-full bg-foreground transition-[width] duration-300"
          style={{ width: `${pct}%` }}
        />
      </div>
    </div>
  );
};

interface BillingCardRowProps extends React.ComponentProps<'div'> {
  label: React.ReactNode;
}

const BillingCardRow = ({ label, className, children, ...props }: BillingCardRowProps) => {
  return (
    <div
      data-slot="billing-card-row"
      className={cn('flex items-center justify-between gap-3 text-sm', className)}
      {...props}
    >
      <span className="text-muted-foreground">{label}</span>
      <span className="text-foreground">{children}</span>
    </div>
  );
};

type BillingCardFooterProps = React.ComponentProps<'div'>;

const BillingCardFooter = ({ className, ...props }: BillingCardFooterProps) => {
  return (
    <div
      data-slot="billing-card-footer"
      className={cn('flex items-center gap-2 border-t border-border pt-4', className)}
      {...props}
    />
  );
};

export {
  BillingCard,
  BillingCardHeader,
  BillingCardEyebrow,
  BillingCardPlan,
  BillingCardPrice,
  BillingCardMeter,
  BillingCardRow,
  BillingCardFooter,
};

const BillingCardBlock = () => {
  return (
    <section data-slot="billing-card-block" className="flex w-full justify-center bg-background p-6 sm:p-10">
      <BillingCard className="w-full max-w-sm">
        <BillingCardHeader>
          <div className="flex flex-col gap-1">
            <BillingCardEyebrow>Current plan</BillingCardEyebrow>
            <BillingCardPlan>Scale</BillingCardPlan>
          </div>
          <BillingCardPrice cycle="mo">$99</BillingCardPrice>
        </BillingCardHeader>

        <BillingCardMeter value={18} max={25} label="Seats used" />

        <div className="flex flex-col gap-2">
          <BillingCardRow label="Renews on">Jul 1, 2026</BillingCardRow>
          <BillingCardRow label="Payment">Visa ending 4242</BillingCardRow>
        </div>

        <BillingCardFooter>
          <Button type="button" variant="outline" className="flex-1">
            Manage billing
          </Button>
          <Button type="button" className="flex-1">
            Upgrade
          </Button>
        </BillingCardFooter>
      </BillingCard>
    </section>
  );
};

export default BillingCardBlock;

Dependencies

shadcn registry

button