Billing invoice history card listing recent invoices with per-status icons and color-coded status badges (Paid, Pending, Overdue, Refunded), invoice numbers, descriptions, dates, currency-formatted amounts, and per-row download buttons. The header surfaces total outstanding balance and an export-all action, with a responsive layout that collapses status badges below amounts on mobile. Built with shadcn/ui Card, Badge, and Button. Perfect for billing dashboards, account settings, and subscription management pages.
import {
ArrowDownToLine,
CheckCircle2,
Clock,
Download,
RotateCcw,
TriangleAlert,
} from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
type InvoiceStatus = "paid" | "pending" | "overdue" | "refunded"
type Invoice = {
id: string
description: string
date: string
amount: number
status: InvoiceStatus
}
const invoices: Invoice[] = [
{
id: "INV-2026-0612",
description: "Pro plan · Monthly",
date: "Jun 1, 2026",
amount: 49,
status: "paid",
},
{
id: "INV-2026-0598",
description: "Additional seats · 3 members",
date: "Jun 1, 2026",
amount: 36,
status: "pending",
},
{
id: "INV-2026-0540",
description: "Pro plan · Monthly",
date: "May 1, 2026",
amount: 49,
status: "overdue",
},
{
id: "INV-2026-0489",
description: "Usage overage · April",
date: "Apr 30, 2026",
amount: 12.4,
status: "refunded",
},
{
id: "INV-2026-0455",
description: "Pro plan · Monthly",
date: "Apr 1, 2026",
amount: 49,
status: "paid",
},
]
const statusConfig: Record<
InvoiceStatus,
{
label: string
icon: typeof CheckCircle2
badgeClass: string
dotClass: string
}
> = {
paid: {
label: "Paid",
icon: CheckCircle2,
badgeClass:
"border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900/60 dark:bg-emerald-950/40 dark:text-emerald-300",
dotClass: "bg-emerald-500",
},
pending: {
label: "Pending",
icon: Clock,
badgeClass:
"border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900/60 dark:bg-amber-950/40 dark:text-amber-300",
dotClass: "bg-amber-500",
},
overdue: {
label: "Overdue",
icon: TriangleAlert,
badgeClass:
"border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-900/60 dark:bg-rose-950/40 dark:text-rose-300",
dotClass: "bg-rose-500",
},
refunded: {
label: "Refunded",
icon: RotateCcw,
badgeClass:
"border-slate-200 bg-slate-50 text-slate-600 dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-300",
dotClass: "bg-slate-400 dark:bg-slate-500",
},
}
const currency = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
})
export default function InvoiceListCard() {
const outstanding = invoices
.filter((i) => i.status === "pending" || i.status === "overdue")
.reduce((sum, i) => sum + i.amount, 0)
return (
<Card
className="w-full max-w-xl gap-0 overflow-hidden rounded-2xl border py-0 shadow-sm"
role="region"
aria-label="Billing invoices"
>
<CardHeader className="flex flex-row items-start justify-between gap-4 space-y-0 border-b px-5 py-5 sm:px-6">
<div className="space-y-1">
<CardTitle className="text-lg font-semibold tracking-tight">
Invoices
</CardTitle>
<CardDescription>
{outstanding > 0 ? (
<>
<span className="font-medium text-rose-600 dark:text-rose-400">
{currency.format(outstanding)}
</span>{" "}
outstanding across {invoices.length} invoices
</>
) : (
<>All {invoices.length} invoices settled</>
)}
</CardDescription>
</div>
<Button
variant="outline"
size="sm"
className="shrink-0 gap-1.5"
aria-label="Download all invoices"
>
<ArrowDownToLine className="size-4" aria-hidden="true" />
<span className="hidden sm:inline">Export all</span>
</Button>
</CardHeader>
<CardContent className="px-0 py-0">
<ul role="list" className="divide-y">
{invoices.map((invoice) => {
const config = statusConfig[invoice.status]
const StatusIcon = config.icon
return (
<li
key={invoice.id}
className="hover:bg-muted/50 flex items-center gap-3 px-5 py-4 transition-colors sm:gap-4 sm:px-6"
>
<div
className="bg-muted text-muted-foreground flex size-10 shrink-0 items-center justify-center rounded-xl"
aria-hidden="true"
>
<StatusIcon className="size-5" />
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<p className="text-foreground truncate text-sm font-medium">
{invoice.id}
</p>
<Badge
variant="outline"
className={`hidden gap-1 rounded-full px-2 py-0 text-[11px] font-medium capitalize sm:inline-flex ${config.badgeClass}`}
>
<span
className={`size-1.5 rounded-full ${config.dotClass}`}
aria-hidden="true"
/>
{config.label}
</Badge>
</div>
<p className="text-muted-foreground mt-0.5 truncate text-xs">
{invoice.description} · {invoice.date}
</p>
</div>
<div className="flex shrink-0 flex-col items-end gap-1">
<span className="text-foreground text-sm font-semibold tabular-nums">
{currency.format(invoice.amount)}
</span>
<Badge
variant="outline"
className={`gap-1 rounded-full px-2 py-0 text-[11px] font-medium capitalize sm:hidden ${config.badgeClass}`}
>
{config.label}
</Badge>
</div>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground hover:text-foreground size-8 shrink-0"
aria-label={`Download invoice ${invoice.id}`}
>
<Download className="size-4" aria-hidden="true" />
</Button>
</li>
)
})}
</ul>
</CardContent>
<CardFooter className="text-muted-foreground justify-between border-t px-5 py-4 text-xs sm:px-6">
<span>Showing {invoices.length} of 38 invoices</span>
<button
type="button"
className="text-foreground font-medium underline-offset-4 hover:underline"
>
View all
</button>
</CardFooter>
</Card>
)
}
INV-2026-0612
PaidPro plan · Monthly · Jun 1, 2026
$49.00PaidINV-2026-0598
PendingAdditional seats · 3 members · Jun 1, 2026
$36.00PendingINV-2026-0540
OverduePro plan · Monthly · May 1, 2026
$49.00OverdueINV-2026-0489
RefundedUsage overage · April · Apr 30, 2026
$12.40RefundedINV-2026-0455
PaidPro plan · Monthly · Apr 1, 2026
$49.00Paid
import {
ArrowDownToLine,
CheckCircle2,
Clock,
Download,
RotateCcw,
TriangleAlert,
} from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
type InvoiceStatus = "paid" | "pending" | "overdue" | "refunded"
type Invoice = {
id: string
description: string
date: string
amount: number
status: InvoiceStatus
}
const invoices: Invoice[] = [
{
id: "INV-2026-0612",
description: "Pro plan · Monthly",
date: "Jun 1, 2026",
amount: 49,
status: "paid",
},
{
id: "INV-2026-0598",
description: "Additional seats · 3 members",
date: "Jun 1, 2026",
amount: 36,
status: "pending",
},
{
id: "INV-2026-0540",
description: "Pro plan · Monthly",
date: "May 1, 2026",
amount: 49,
status: "overdue",
},
{
id: "INV-2026-0489",
description: "Usage overage · April",
date: "Apr 30, 2026",
amount: 12.4,
status: "refunded",
},
{
id: "INV-2026-0455",
description: "Pro plan · Monthly",
date: "Apr 1, 2026",
amount: 49,
status: "paid",
},
]
const statusConfig: Record<
InvoiceStatus,
{
label: string
icon: typeof CheckCircle2
badgeClass: string
dotClass: string
}
> = {
paid: {
label: "Paid",
icon: CheckCircle2,
badgeClass:
"border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900/60 dark:bg-emerald-950/40 dark:text-emerald-300",
dotClass: "bg-emerald-500",
},
pending: {
label: "Pending",
icon: Clock,
badgeClass:
"border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900/60 dark:bg-amber-950/40 dark:text-amber-300",
dotClass: "bg-amber-500",
},
overdue: {
label: "Overdue",
icon: TriangleAlert,
badgeClass:
"border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-900/60 dark:bg-rose-950/40 dark:text-rose-300",
dotClass: "bg-rose-500",
},
refunded: {
label: "Refunded",
icon: RotateCcw,
badgeClass:
"border-slate-200 bg-slate-50 text-slate-600 dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-300",
dotClass: "bg-slate-400 dark:bg-slate-500",
},
}
const currency = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
})
export default function InvoiceListCard() {
const outstanding = invoices
.filter((i) => i.status === "pending" || i.status === "overdue")
.reduce((sum, i) => sum + i.amount, 0)
return (
<Card
className="w-full max-w-xl gap-0 overflow-hidden rounded-2xl border py-0 shadow-sm"
role="region"
aria-label="Billing invoices"
>
<CardHeader className="flex flex-row items-start justify-between gap-4 space-y-0 border-b px-5 py-5 sm:px-6">
<div className="space-y-1">
<CardTitle className="text-lg font-semibold tracking-tight">
Invoices
</CardTitle>
<CardDescription>
{outstanding > 0 ? (
<>
<span className="font-medium text-rose-600 dark:text-rose-400">
{currency.format(outstanding)}
</span>{" "}
outstanding across {invoices.length} invoices
</>
) : (
<>All {invoices.length} invoices settled</>
)}
</CardDescription>
</div>
<Button
variant="outline"
size="sm"
className="shrink-0 gap-1.5"
aria-label="Download all invoices"
>
<ArrowDownToLine className="size-4" aria-hidden="true" />
<span className="hidden sm:inline">Export all</span>
</Button>
</CardHeader>
<CardContent className="px-0 py-0">
<ul role="list" className="divide-y">
{invoices.map((invoice) => {
const config = statusConfig[invoice.status]
const StatusIcon = config.icon
return (
<li
key={invoice.id}
className="hover:bg-muted/50 flex items-center gap-3 px-5 py-4 transition-colors sm:gap-4 sm:px-6"
>
<div
className="bg-muted text-muted-foreground flex size-10 shrink-0 items-center justify-center rounded-xl"
aria-hidden="true"
>
<StatusIcon className="size-5" />
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<p className="text-foreground truncate text-sm font-medium">
{invoice.id}
</p>
<Badge
variant="outline"
className={`hidden gap-1 rounded-full px-2 py-0 text-[11px] font-medium capitalize sm:inline-flex ${config.badgeClass}`}
>
<span
className={`size-1.5 rounded-full ${config.dotClass}`}
aria-hidden="true"
/>
{config.label}
</Badge>
</div>
<p className="text-muted-foreground mt-0.5 truncate text-xs">
{invoice.description} · {invoice.date}
</p>
</div>
<div className="flex shrink-0 flex-col items-end gap-1">
<span className="text-foreground text-sm font-semibold tabular-nums">
{currency.format(invoice.amount)}
</span>
<Badge
variant="outline"
className={`gap-1 rounded-full px-2 py-0 text-[11px] font-medium capitalize sm:hidden ${config.badgeClass}`}
>
{config.label}
</Badge>
</div>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground hover:text-foreground size-8 shrink-0"
aria-label={`Download invoice ${invoice.id}`}
>
<Download className="size-4" aria-hidden="true" />
</Button>
</li>
)
})}
</ul>
</CardContent>
<CardFooter className="text-muted-foreground justify-between border-t px-5 py-4 text-xs sm:px-6">
<span>Showing {invoices.length} of 38 invoices</span>
<button
type="button"
className="text-foreground font-medium underline-offset-4 hover:underline"
>
View all
</button>
</CardFooter>
</Card>
)
}