BlocksUpdated September 15, 2026

Payment method

A presentation-only payment-method panel with consumer-owned method, expiry, contact, status and actions.

Present payment details without owning payment behaviorLink to section

PaymentMethodPanel gives an application a bounded surface for displaying the payment method it already knows about. Use it for method, expiry, billing contact, status and consumer-owned actions while your billing provider and server remain the source of truth.

No payment mutation inside the Block

The Block never stores card data, opens Stripe, attaches or detaches a payment method, validates billing details or decides whether an update is allowed. Those operations remain application-owned.

Canonical identityLink to section

  • Category: Commerce, slug commerce.
  • Block: payment-method, export PaymentMethodPanel.
  • Canonical source: apps/marketing/content/blocks/commerce/payment-method/.
  • Complete entry point: index.tsx.

Copy sourceLink to section

Configure the public UI package and tokens, then create src/components/blocks/payment-method/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/payment-method/index.tsx
import * as React from "react";

export type PaymentMethodPanelProps = Readonly<{
  title: React.ReactNode;
  description?: React.ReactNode;
  methodLabel: React.ReactNode;
  methodValue: React.ReactNode;
  expiryLabel?: React.ReactNode;
  expiryValue?: React.ReactNode;
  contactLabel?: React.ReactNode;
  contactValue?: React.ReactNode;
  status?: React.ReactNode;
  primaryAction?: React.ReactNode;
  secondaryAction?: React.ReactNode;
  className?: string;
}>;

/**
 * Presentation-only payment-method surface. Billing truth and every action
 * remain entirely owned by the consuming application.
 */
export function PaymentMethodPanel({
  title,
  description,
  methodLabel,
  methodValue,
  expiryLabel,
  expiryValue,
  contactLabel,
  contactValue,
  status,
  primaryAction,
  secondaryAction,
  className,
}: PaymentMethodPanelProps) {
  const id = React.useId();
  const titleId = `${id}-title`;
  const descriptionId = `${id}-description`;
  const rootClassName = [
    "min-w-0 space-y-6 rounded-xl border border-border bg-card p-5 text-card-foreground sm:p-6",
    className,
  ]
    .filter(Boolean)
    .join(" ");

  const details = [
    { label: methodLabel, value: methodValue },
    expiryLabel && expiryValue
      ? { label: expiryLabel, value: expiryValue }
      : null,
    contactLabel && contactValue
      ? { label: contactLabel, value: contactValue }
      : null,
  ].filter(
    (
      detail,
    ): detail is Readonly<{
      label: React.ReactNode;
      value: React.ReactNode;
    }> => detail !== null,
  );

  return (
    <section
      aria-describedby={description ? descriptionId : undefined}
      aria-labelledby={titleId}
      className={rootClassName}
      data-slot="payment-method-panel"
    >
      <header className="min-w-0 space-y-2">
        <div className="flex min-w-0 flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
          <div 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}
          </div>
          {status ? <div className="shrink-0">{status}</div> : null}
        </div>
      </header>

      <dl className="grid min-w-0 gap-4 sm:grid-cols-2">
        {details.map((detail, index) => (
          <div
            className="min-w-0 rounded-lg border border-border bg-background p-4"
            key={index}
          >
            <dt className="text-sm font-medium text-muted-foreground">
              {detail.label}
            </dt>
            <dd className="mt-1 break-words text-sm font-medium">
              {detail.value}
            </dd>
          </div>
        ))}
      </dl>

      {primaryAction || secondaryAction ? (
        <div className="flex min-w-0 flex-col gap-3 sm:flex-row sm:flex-wrap">
          {primaryAction}
          {secondaryAction}
        </div>
      ) : null}
    </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 payment-method directory into your application-owned Blocks path.
  2. Configure the public PyColors UI package and tokens through the UI installation guide.
  3. Supply payment facts, status and actions from your validated billing layer.
  4. Run lint, type-check, tests and build, then review real labels, long values, narrow widths, both themes and keyboard behavior.
src/components/payment-method-summary.tsx
"use client";

import { PaymentMethodPanel } from "./blocks/payment-method";

export function PaymentMethodSummary() {
  return (
    <PaymentMethodPanel
      title="Payment method"
      description="Example values supplied by your application."
      methodLabel="Method"
      methodValue="Visa ending in 4242"
      expiryLabel="Expires"
      expiryValue="10 / 28"
      contactLabel="Billing contact"
      contactValue="billing@example.com"
      status={<span>Default</span>}
      primaryAction={
        <button type="button" onClick={() => undefined}>
          Update payment method
        </button>
      }
    />
  );
}

The example action performs no payment operation. Replace it with your own provider-backed flow only after validating permissions and current account state.

Consumer-owned contractLink to section

title, methodLabel and methodValue are required. Description, expiry and contact pairs are optional. status can present consumer-owned context such as a default-method badge, but it does not carry billing semantics by itself.

primaryAction and secondaryAction are rendered exactly as supplied. The Block does not clone controls, intercept events or infer whether a user may edit or remove a method.

Responsive and accessibility notesLink to section

The panel uses a labelled section, semantic definition-list markup and a responsive details grid. Keep card or bank identifiers appropriately redacted, provide meaningful control names and preserve visible focus and native disabled semantics in the actions you supply.

Ownership and boundariesLink to section

Your application owns provider tokens, PCI-sensitive handling, billing contacts, payment-method lifecycle, permissions, mutations, redirects and errors. The Block does not include Stripe Elements, SetupIntents, Customer Portal, checkout, webhooks, persistence, authentication, analytics, Registry or CLI behavior.

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