BlocksUpdated September 15, 2026

Invoice history

A presentation-only invoice table with consumer-owned dates, amounts, statuses, actions and empty state.

Present invoice history without owning invoice retrievalLink to section

InvoiceHistoryPanel presents invoice records supplied by your application in a readable table with an explicit empty state. Your billing backend remains responsible for fetching records, formatting monetary truth and authorizing any download or detail action.

Consumer-owned invoice data

The Block does not fetch invoices, generate documents, format currency, resolve payment status or authorize downloads. Pass already-resolved display values and connect actions through your own validated billing boundary.

Canonical identityLink to section

  • Category: Commerce, slug commerce.
  • Block: invoice-history, export InvoiceHistoryPanel.
  • Canonical source: apps/marketing/content/blocks/commerce/invoice-history/.
  • Complete entry point: index.tsx.

Copy sourceLink to section

Configure the public UI package and tokens, then create src/components/blocks/invoice-history/index.tsx in your application.

Open the complete source below and use the code block copy button when available. If clipboard access is unavailable, select the code manually.

View and copy the complete source
src/components/blocks/invoice-history/index.tsx
import * as React from "react";

export type InvoiceHistoryItem = Readonly<{
  id: string;
  date: React.ReactNode;
  amount: React.ReactNode;
  status?: React.ReactNode;
  action?: React.ReactNode;
}>;

export type InvoiceHistoryPanelProps = Readonly<{
  title: React.ReactNode;
  description?: React.ReactNode;
  invoices: readonly InvoiceHistoryItem[];
  dateLabel?: React.ReactNode;
  amountLabel?: React.ReactNode;
  statusLabel?: React.ReactNode;
  actionLabel?: React.ReactNode;
  emptyState?: React.ReactNode;
  className?: string;
}>;

/**
 * Presentation-only invoice history surface. Invoice data, retrieval and every
 * action remain entirely owned by the consuming application.
 */
export function InvoiceHistoryPanel({
  title,
  description,
  invoices,
  dateLabel = "Date",
  amountLabel = "Amount",
  statusLabel = "Status",
  actionLabel = "Action",
  emptyState = "No invoices yet.",
  className,
}: InvoiceHistoryPanelProps) {
  const id = React.useId();
  const titleId = `${id}-title`;
  const descriptionId = `${id}-description`;
  const rootClassName = [
    "min-w-0 space-y-5 rounded-xl border border-border bg-card p-5 text-card-foreground sm:p-6",
    className,
  ]
    .filter(Boolean)
    .join(" ");

  return (
    <section
      aria-describedby={description ? descriptionId : undefined}
      aria-labelledby={titleId}
      className={rootClassName}
      data-slot="invoice-history-panel"
    >
      <header className="min-w-0 space-y-2">
        <h2 className="break-words text-xl font-semibold" id={titleId}>
          {title}
        </h2>
        {description ? (
          <div
            className="break-words text-sm text-muted-foreground"
            id={descriptionId}
          >
            {description}
          </div>
        ) : null}
      </header>

      {invoices.length === 0 ? (
        <div
          className="rounded-lg border border-dashed border-border bg-background p-5 text-sm text-muted-foreground"
          data-slot="invoice-history-empty"
        >
          {emptyState}
        </div>
      ) : (
        <div className="min-w-0 overflow-x-auto rounded-lg border border-border">
          <table className="w-full min-w-[36rem] border-collapse text-left text-sm">
            <thead className="bg-muted/50 text-muted-foreground">
              <tr>
                <th className="px-4 py-3 font-medium" scope="col">
                  {dateLabel}
                </th>
                <th className="px-4 py-3 font-medium" scope="col">
                  {amountLabel}
                </th>
                <th className="px-4 py-3 font-medium" scope="col">
                  {statusLabel}
                </th>
                <th className="px-4 py-3 text-right font-medium" scope="col">
                  {actionLabel}
                </th>
              </tr>
            </thead>
            <tbody className="divide-y divide-border">
              {invoices.map((invoice) => (
                <tr className="bg-background align-middle" key={invoice.id}>
                  <td className="max-w-48 break-words px-4 py-3">
                    {invoice.date}
                  </td>
                  <td className="max-w-48 break-words px-4 py-3 font-medium">
                    {invoice.amount}
                  </td>
                  <td className="max-w-48 break-words px-4 py-3">
                    {invoice.status ?? "—"}
                  </td>
                  <td className="px-4 py-3 text-right">
                    {invoice.action ? (
                      <div className="inline-flex min-w-0 justify-end">
                        {invoice.action}
                      </div>
                    ) : (
                      <span aria-hidden="true">—</span>
                    )}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </section>
  );
}

The documentation includes the canonical file at build time. Your application owns the copied source and does not receive automatic updates. Review future changes deliberately and validate it in your application before adoption. Return to the Blocks catalog to compare another pattern.

Install by copying sourceLink to section

  1. Copy the complete invoice-history directory into your application-owned Blocks path.
  2. Configure the public PyColors UI package and tokens through the UI installation guide.
  3. Supply invoice rows from your own billing data and connect any row action.
  4. Run lint, type-check, tests and build, then review overflow, long values, empty data, both themes and keyboard behavior.
src/components/invoice-history.tsx
"use client";

import { InvoiceHistoryPanel } from "./blocks/invoice-history";

const invoices = [
  {
    id: "invoice-example-1",
    date: "September 1, 2026",
    amount: "$49.00",
    status: "Paid",
    action: <a href="/billing/invoices/example">View invoice</a>,
  },
] as const;

export function InvoiceHistory() {
  return (
    <InvoiceHistoryPanel
      title="Invoice history"
      description="Example records supplied by your application."
      invoices={invoices}
      emptyState="No invoices are available."
    />
  );
}

The example destination is application-owned. Connect invoice files or provider records only through a route that rechecks authorization on the server.

Consumer-owned contractLink to section

title and invoices are required. Each invoice needs a stable id, display date and display amount; status and action are optional. Column labels and the empty state can be replaced with product-specific copy.

The Block renders the supplied values without calculating totals, formatting currency, translating provider states or deciding whether an invoice action is available. Keep row IDs stable and make action labels specific to their result.

Responsive and accessibility notesLink to section

The panel uses a labelled section and a semantic table when records exist. The table keeps a readable minimum width inside a horizontal overflow container so content is not crushed on narrow screens. Test the containing layout and ensure keyboard users can reach every interactive row action without losing context.

Ownership and boundariesLink to section

Your application owns invoice retrieval, currency formatting, tax and billing truth, document storage, signed URLs, authorization, provider integration, mutations and error handling. The Block does not include invoice generation, download services, Stripe APIs, checkout, webhooks, persistence, authentication, analytics, Registry or CLI behavior.

Explore the Blocks catalog, compare Billing overview, or compare Starters for a complete application foundation.