> ## Documentation Index
> Fetch the complete documentation index at: https://docs.whop.com/llms.txt
> Use this file to discover all available pages before exploring further.

# CardFields

> The exploded card unit: three individually mountable, PCI-isolated fields (number, expiration, security code) under one completeness surface — for layouts the drop-in card element can't express. Reached through `payments.create("cardFields")`: place each field yourself, drive your pay button off `onChange`, confirm with `payments.createConfirmationToken()`. Card numbers never touch your page or ours.

{/* whop-elements-docs-channel: upcoming */}

<Info>**Upcoming** — generated from the latest merged element source; documents unreleased development. Use the channel picker at the top of the sidebar for the docs of a published release.</Info>

{/* whop-elements-since: unreleased */}*In development — not yet part of a stable release.*

A **sub-controller** in the [`Payments`](/elements/upcoming/payments/overview) group: mint it with `payments.create('cardFields', { … })` (vanilla) or render `<CardFields>` inside `<Payments>` (React — mount mints, unmount destroys), then mount its elements off the minted handle. `destroy()` tears it down and frees its place for the alternatives — a later `create('cardFields')` boots fresh.

<Note>**Exclusive** — when using `CardFields`, you can't mount `PaymentElement` or `CardElement` in the same Payments handle: these are alternatives — mount one at a time; destroy it to mount another.</Note>

## Preview

A live, interactive demo of this sub-controller's default arrangement with sample data:

<div data-whop-demo-shell style={{ position: "relative", minHeight: "480px", transition: "min-height 200ms ease" }}>
  <div data-whop-demo-skeleton style={{ position: "absolute", inset: "0", borderRadius: "12px", background: "rgba(140, 140, 140, 0.12)", pointerEvents: "none", transition: "opacity 200ms ease" }} />

  <iframe data-whop-demo src="https://js.whop.cloud/elements/amber/card-fields/_unit/index.html" title="CardFields demo" height="480" allow="payment; publickey-credentials-get; clipboard-write" style={{ position: "relative", width: "100%", border: "0", borderRadius: "12px", background: "transparent", colorScheme: "normal" }} />
</div>

## Usage

<div data-whop-usage="payments/cardFields">
  <CodeGroup>
    ```tsx React theme={null}
    import { WhopElements, Payments, CardFields, CardFieldsCardNumberElement, CardFieldsCardExpiryElement, CardFieldsCardCvcElement } from "@whop/elements-react";
    import { loadWhop } from "@whop/elements";

    function Example() {
      return (
        <WhopElements elements={loadWhop()}>
          <Payments /* options */>
            <CardFields>
              <CardFieldsCardNumberElement />
              <CardFieldsCardExpiryElement />
              <CardFieldsCardCvcElement />
            </CardFields>
          </Payments>
        </WhopElements>
      );
    }
    ```

    ```html Vanilla theme={null}
    <script src="https://js.whop.cloud/elements/amber/elements.js" data-whop-elements></script>
    <script type="module">
      const payments = window.WhopElements().payments.create({ /* options */ });
      const cardFields = payments.create('cardFields', { /* options */ });
      cardFields.create('cardNumber').mount('#payments-cardFields-cardNumber');
      cardFields.create('cardExpiry').mount('#payments-cardFields-cardExpiry');
      cardFields.create('cardCvc').mount('#payments-cardFields-cardCvc');
    </script>
    ```
  </CodeGroup>
</div>

## Options

Pass these to `payments.create('cardFields', { … })` — or as props on `<CardFields>` in React. Parent-injected props never appear here.

<ResponseField name="layout" type="&#x22;compact&#x22; | &#x22;stacked&#x22;">
  How the composed card faces draw their cell chrome: 'stacked' (default) shapes the number as the top cell with expiration and security code as the bottom row; 'compact' shapes all three as segments of one single-row container. The fused card element forwards its own `layout`; on the exploded face you own where each field mounts and every field draws fully independent chrome — all corners rounded, full border. Defaults to `"stacked"`.
</ResponseField>

<ResponseField name="publicKey" type="string">
  Advanced: a Basis Theory publishable key to use instead of reading the matrix card entry. The composing card faces supply it from the matrix they already hold; omitted, the unit fetches its own.
</ResponseField>

## Events

Pass a callback in the create options / React props.

### `onChange`

Field completeness changed — `complete: true` means all three fields are filled so the confirm can run; drive your pay button off it. `brand` is the detected card network.

**Signature:** `((payload: { complete: boolean; brand: string; }) => void)`

### `onLoadingChange`

Fired when the grouped loading state changes — `true` while any mounted element is still loading.

**Signature:** `((loading: boolean) => void)`

## Methods

Call these on the minted sub handle — the return of `payments.create('cardFields', { … })`.

### `tokenize`

Tokenize the three mounted fields into a card credential WITHOUT minting a confirmation token (also emitted to the declaring controller as `tokenized`) — the advanced seam; confirming a payment wants `payments.createConfirmationToken`. Validation failures land on the failing field and throw a legible error.

**Signature:** `(input: { accountId?: string | undefined; apiBaseUrl?: string | undefined; }) => Promise<{ token: string; }>`

### `collect`

The exploded face's collection — `payments.createConfirmationToken` (the confirm cascade) is the verb to call; this surface action is where its cardFields branch lands: tokenizes the fields and returns the collected bag the controller mints from.

**Signature:** `(input: { billingDetails?: { email?: string | undefined; name?: string | undefined; address?: { country?: string | undefined; line1?: string | undefined; city?: string | undefined; postal_code?: string | undefined; } | undefined; } | undefined; }) => Promise<{ paymentMethod: { type: string; category: string; card: { token: string; }; }; billingDetails: { email: string; name?: string | undefined; address?: { country?: string | undefined; line1?: string | undefined; city?: string | undefined; postal_code?: string | undefined; } | undefined; }; }>`

### `update`

Live-update the sub-controller's consumer props and `on<Event>` callbacks.

**Signature:** `(options: Partial<CardFieldsSubOptions>) => void`

### `destroy`

Tear the sub-controller down: its mounted elements are destroyed, its runtime state is discarded, and its exclusive slot is freed — a later `create("cardFields")` boots fresh.

**Signature:** `() => void`

## Elements

The elements this sub-controller mounts — each has its own page:

<CardGroup cols={2}>
  <Card title="CardFieldsCardNumberElement" href="/elements/upcoming/payments/cardFields-cardNumber">
    The PCI card number field — a Basis Theory hosted input under frosted chrome. A child of the card unit; the card number never touches this page.
  </Card>

  <Card title="CardFieldsCardExpiryElement" href="/elements/upcoming/payments/cardFields-cardExpiry">
    The PCI card expiration field — a Basis Theory hosted input under frosted chrome. A child of the card unit.
  </Card>

  <Card title="CardFieldsCardCvcElement" href="/elements/upcoming/payments/cardFields-cardCvc">
    The PCI security code field — a Basis Theory hosted input under frosted chrome. A child of the card unit.
  </Card>
</CardGroup>
