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

# Elements

> Build checkout into your own page - cart, email, payment methods and the pay button as separate pieces you place and style yourself

<Note>
  **Elements is in public beta.** It works and it takes real payments, but the API can still change
  and a few things are missing (see [Known limits](#known-limits) at the bottom). If you build on it
  now, pin a copy of your integration code somewhere you can find it again, and tell us what breaks.
</Note>

## What this is

The [Embed SDK](/guides/embed-sdk) opens our checkout in a modal on top of your page. Elements is the
other option: you get the checkout as separate pieces - the cart summary, the email field, the
payment method list, the pay button - and you decide where each one goes and what it looks like.

The obvious question is why you'd want that when the modal already works. Two reasons, and if
neither applies to you, use the Embed SDK, it's less work:

* **The checkout is part of your page**, not a layer over it. It sits in your layout, in your
  column, next to your product photography.
* **It wears your brand.** Your colours, your typeface, your corner radius, your spacing.

The trade-off is that you're now responsible for the layout, and there's more to get wrong.

<Card title="Live demo" icon="laptop-binary" href="https://checkout.komerza.com/elements/demo.html">
  A pretend shop with the all-in-one element in its sidebar, wearing that shop's brand
</Card>

## Quick start

One script tag, no build step, no package to install:

```html theme={null}
<script src="https://checkout.komerza.com/elements/elements.iife.js"></script>
```

Then a div to mount into, and four lines:

```html theme={null}
<div id="checkout"></div>

<script>
  const komerza = Komerza();

  const elements = komerza.elements({
    items: [{ productId: "YOUR_PRODUCT_ID", variantId: "YOUR_VARIANT_ID", quantity: 1 }]
  });

  elements.create("checkout").mount("#checkout");
</script>
```

That's a complete, working checkout: cart, email, payment methods, terms, and a pay button. It's the
`checkout` element - everything in one piece. Most integrations should start here and only split it
up if they actually need to.

## The elements

`elements.create(type)` takes one of these:

| Type             | What it renders                                                            |
| ---------------- | -------------------------------------------------------------------------- |
| `checkout`       | All of it in one element - cart, email, payment methods, terms, pay button |
| `summary`        | The cart: line items, quantity controls, discounts, total                  |
| `email`          | The buyer's email address, with validation                                 |
| `payment`        | The payment method list, and the chosen gateway rendered in place          |
| `coupon`         | A coupon code field                                                        |
| `consent`        | Terms acceptance and the marketing opt-in                                  |
| `billingAddress` | Billing address, where the store or gateway requires one                   |
| `customFields`   | Any custom fields configured on the product                                |

Every element you create from the same `elements(...)` call shares one cart. Change the quantity in
`summary` and the total in `checkout` updates on its own - you don't wire that up.

```js theme={null}
const elements = komerza.elements({ items: [...] });

elements.create("summary").mount("#summary");
elements.create("email").mount("#email");
elements.create("payment").mount("#payment");
```

## How it works

Worth two minutes, because it explains most of the rules further down.

Each element is an iframe served from `checkout.komerza.com`. Card numbers, the buyer's email and
the order itself never touch your page or your JavaScript - which is the point, and it's what keeps
your PCI scope where it is.

Alongside your elements we mount one hidden frame, the **controller**. It owns the cart, talks to our
backend, and is the only thing allowed to create an order. Your elements don't talk to each other
directly; they send changes to the controller and it tells everyone what the new state is. That's why
two elements can never disagree about the total.

Two consequences you'll actually notice:

* **Your JavaScript can't reach inside an element.** Cross-origin, deliberately. Everything you need
  comes through `elements.on(...)` and the methods below.
* **Elements paint no background of their own**, in any theme. They inherit whatever's behind them,
  so a checkout in a white card looks like part of that card.

## Styling

This is the part Elements exists for.

### Tokens

`tokens` are the broad strokes. Set an accent colour and we regenerate the whole primary ramp from
it, so it reaches everything that reads that ramp rather than just the pay button.

```js theme={null}
const elements = komerza.elements({
  items: [...],
  brand: {
    mode: "light",              // "light" | "dark" | "auto" (default)
    tokens: {
      accent: "#b2452a",        // hex - the full ramp is derived from this
      text: "#221f1c",
      textMuted: "#6b635a",
      border: "#ddd5c9",
      surface: "#ffffff",
      surfaceSunken: "#f7f5f2",
      danger: "#b91c1c",
      success: "#15803d",
      warning: "#b45309",
      radius: "2px",
      gap: "1rem",
      font: "Inter, system-ui, sans-serif",
      fontMono: "ui-monospace, Menlo, monospace"
    }
  }
});
```

<Warning>
  Set `mode` explicitly if your page is always light or always dark. The default follows the
  buyer's OS setting, so a permanently light page viewed by someone in dark mode gets a dark
  checkout on a white panel.
</Warning>

### Fonts

An element is a separate document from your page, so a font your page loads is **not** available to
it - the checkout will quietly fall back to a system face. Load fonts into the elements explicitly:

```js theme={null}
brand: {
  fonts: [
    "Inter:wght@400;500;600",
    { family: "Fraunces", weights: [500, 700] },
    "https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@700"
  ],
  tokens: { font: "Inter, system-ui, sans-serif" }
}
```

Google Fonts only, and we rebuild the URL from the parts rather than using what you pass. Any other
stylesheet would be applied to a payment page without passing through the rules sanitizer below,
which would defeat the whole thing.

### Rules

`rules` is CSS, scoped to a fixed vocabulary of class names:

```js theme={null}
brand: {
  rules: {
    ".kmrza-button": { "border-radius": "2px", "letter-spacing": "0.04em" },
    ".kmrza-field-input": { "border-radius": "2px", "border-color": "#ddd5c9" },
    ".kmrza-method": { "border-radius": "2px" },
    ".kmrza-method[data-selected]": {
      "border-color": "#b2452a",
      "background-color": "#fbf3f0"
    },
    ".kmrza-total-amount": { "font-family": "Fraunces, Georgia, serif" }
  }
}
```

Selectors must be a single class from the list below, optionally narrowed by one state attribute
(`[data-selected]`, `[data-disabled]`, `[data-loading]`, `[data-invalid]`, `[data-locked]`) and one
pseudo-class (`:hover`, `:focus`, `:focus-visible`, `:active`, `:disabled`, `:checked`). No
descendant selectors, no element names, no ids.

That's narrower than real CSS on purpose. A checkout that can be restyled arbitrarily can be made to
lie about what it charges - hide the total, cover the pay button, dress a decline up as a success. So
a handful of properties are refused (`position`, `content`, `z-index`, `pointer-events`, `transform`,
`clip-path`, and anything that fetches a URL), and `!important` is stripped. Everything cosmetic is
yours.

**Anything we refuse is reported, never dropped silently:**

```js theme={null}
elements.on("trace", (e) => console.warn(e.message));
// → brand: Rule ".foo" refused - .foo is not a Komerza hook.
```

### The class names

| Group         | Classes                                                                                                                                                        |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Fields        | `.kmrza-field` `.kmrza-field-label` `.kmrza-field-input` `.kmrza-field-hint` `.kmrza-field-error`                                                              |
| Buttons       | `.kmrza-button` `.kmrza-button-primary` `.kmrza-button-secondary` `.kmrza-button-outline` `.kmrza-button-ghost` `.kmrza-button-danger`                         |
| Cart          | `.kmrza-summary` `.kmrza-line-item` `.kmrza-line-item-image` `.kmrza-line-item-name` `.kmrza-line-item-option` `.kmrza-line-item-qty` `.kmrza-line-item-price` |
| Totals        | `.kmrza-discount` `.kmrza-total` `.kmrza-total-label` `.kmrza-total-amount`                                                                                    |
| Payment       | `.kmrza-payment` `.kmrza-method-list` `.kmrza-method` `.kmrza-method-icon` `.kmrza-method-label` `.kmrza-method-badge` `.kmrza-method-panel`                   |
| Gateway UI    | `.kmrza-panel` `.kmrza-heading` `.kmrza-subheading` `.kmrza-alert` `.kmrza-alert-title` `.kmrza-alert-text` `.kmrza-badge` `.kmrza-spinner`                    |
| Element roots | `.kmrza-checkout` `.kmrza-coupon` `.kmrza-consent` `.kmrza-billing` `.kmrza-custom-fields` `.kmrza-protection` `.kmrza-skeleton`                               |
| Misc          | `.kmrza-email` `.kmrza-disclosure` `.kmrza-interval` `.kmrza-overlay` `.kmrza-overlay-panel` `.kmrza-overlay-close`                                            |

These are a promise. Once a class is on this list it keeps existing and keeps meaning the same thing,
even as we rewrite what's behind it.

### Starting from nothing

If you'd rather build the whole appearance yourself, `preset: "bare"` strips our cosmetics down to
structure - no borders, no backgrounds, no radius, no padding:

```js theme={null}
brand: { preset: "bare", rules: { /* all yours */ } }
```

### Changing the brand later

```js theme={null}
elements.update({ brand: { tokens: { accent: "#0f766e" } } });
```

Restyles in place. No remount, no flash.

## Events

```js theme={null}
elements.on("error", (e) => {
  console.error(e.type, e.code, e.message);
});
```

| Event       | When                                                                        |
| ----------- | --------------------------------------------------------------------------- |
| `ready`     | An element has loaded and is interactive                                    |
| `change`    | The buyer changed something - method chosen, quantity edited, email entered |
| `error`     | Anything went wrong, anywhere                                               |
| `resize`    | An element changed height                                                   |
| `focus`     | An element took focus                                                       |
| `blur`      | An element lost focus                                                       |
| `navigate`  | The buyer is being handed to a gateway that needs its own window            |
| `trace`     | Diagnostics, including refused brand rules                                  |
| `loaderror` | An element's frame failed to load at all                                    |

`error` is the one to handle. Everything that can fail arrives there in one shape, so you don't have
to watch each element separately:

```js theme={null}
elements.on("error", (e) => {
  // e.type   - "load" | "session" | "validation" | "order" | "payment"
  // e.code   - the backend's own code, when there is one: "OUT_OF_STOCK",
  //            "EMAIL_DOMAIN_REJECTED", "COUPON_INVALID"
  // e.element - which element raised it, if it came from one
  // e.recoverable - true when the buyer can just try again
});
```

Handle `e.code` where you can. It's the part that tells you what to actually do - "out of stock"
wants a different response from "that card was declined".

## Taking payment

The `checkout` element has its own pay button and drives the whole thing. Nothing else to do.

If you've split the elements up and want your own button, call `confirmPayment` yourself:

```js theme={null}
const result = await komerza.confirmPayment({
  elements,
  returnUrl: "https://yourshop.com/thank-you"
});

if (result.error) {
  console.error(result.error.code, result.error.message);
} else {
  console.log("order", result.order.id);
}
```

`returnUrl` matters for the gateways that take over the whole window - without it there's nowhere to
send the buyer back to. Most gateways render inline and never leave your page, but some can't.

### Letting the buyer change their mind

Once an order exists the cart is locked, so a buyer who spots a typo isn't stuck:

```js theme={null}
elements.edit(); // unlocks the cart and returns to the form
```

## A complete example

```html theme={null}
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Feldspar Supply</title>
  </head>
  <body>
    <main>
      <h1>Kiln-fired stoneware</h1>
      <div id="checkout"></div>
    </main>

    <script src="https://checkout.komerza.com/elements/elements.iife.js"></script>
    <script>
      const komerza = Komerza();

      const elements = komerza.elements({
        items: [
          { productId: "0f6294be-25d6-4ced-96df-7d500608c54d",
            variantId: "96f846e5-5f22-46a2-97b3-a1c88f685577",
            quantity: 1 }
        ],
        locale: "en",
        brand: {
          mode: "light",
          fonts: ["Inter:wght@400;500;600"],
          tokens: {
            accent: "#b2452a",
            text: "#221f1c",
            border: "#ddd5c9",
            radius: "2px",
            font: "Inter, system-ui, sans-serif"
          },
          rules: {
            ".kmrza-button": { "border-radius": "2px" },
            ".kmrza-field-input": { "border-radius": "2px" },
            ".kmrza-method": { "border-radius": "2px" },
            ".kmrza-method[data-selected]": { "border-color": "#b2452a" }
          }
        }
      });

      // showSummary opts into the product details; the default is a compact total
      elements.create("checkout", { showSummary: true }).mount("#checkout");

      elements.on("error", (e) => console.error(e.type, e.code, e.message));
    </script>
  </body>
</html>
```

## Content Security Policy

If your site sends a CSP, elements need two directives:

```
frame-src https://checkout.komerza.com;
script-src https://checkout.komerza.com;
```

Using nonces? Pass yours in and we'll put it on every frame we create:

```js theme={null}
const komerza = Komerza(undefined, { nonce: "abc123" });
```

## Known limits

Straight list of what isn't there yet, so you don't find out the hard way.

* **No npm package or React wrapper.** Script tag only for now.
* **No currency selector element.** Adaptive currency still works; there just isn't a picker.
* **The API can change.** It's a beta. We'll tell you before anything breaks, but treat it as
  provisional.

## Which one should I use?

<CardGroup cols={2}>
  <Card title="Use the Embed SDK" icon="window">
    You want a buy button that opens checkout in a modal. It's a script tag and a data attribute,
    and it's done in five minutes.
  </Card>

  <Card title="Use Elements" icon="table-columns">
    You want checkout inside your page, laid out and styled as part of your design, and you're
    willing to own the layout.
  </Card>
</CardGroup>
