> For the complete documentation index, see [llms.txt](https://docs.peakcommerce.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.peakcommerce.com/developers/component-cookbook.md).

# Component Cookbook

Copy-paste recipes for custom components, from static layouts to live-data and journey-aware blocks. Each one is real code you can paste into the **Code** view of the component editor (**Content → Component Library**) and adapt. For the underlying model — the `{{field}}` format, data bindings, and the sandbox rules — see [Building Custom Components](/developers/custom-components.md).

## Two ways to use data

Components reach data two ways, and the recipes below use both:

* **Declarative bindings — `{{data.<name>.path}}`.** Resolved on the server at render time and substituted into your HTML. Best for **scalar values** you want to print (a name, a balance, a status). Identity is supplied by the platform, scope-checked and audited.
* **The runtime bridge — `window.PC.call(channel, action, params)`.** A narrow postMessage RPC injected into every component. Best for **interactivity** — reading or advancing the journey, resolving live pricing, fetching the catalog. The sandbox sets `connect-src 'none'`, so `window.PC.call` is the *only* way a component reaches out; direct `fetch` is blocked.

***

## Recipe 1 — A field-driven CTA hero

A pure-layout block (no data) whose text and link are author-editable fields. The simplest kind of component, and the most common.

```html
<style>
  .hero { padding: 56px 32px; border-radius: 16px; background: linear-gradient(135deg,#5B4BD6,#4A3CC0); color: #fff; text-align: center; font-family: system-ui, -apple-system, sans-serif; }
  .hero h1 { font-size: 2rem; margin: 0 0 12px 0; }
  .hero p { font-size: 1.0625rem; opacity: 0.9; margin: 0 0 24px 0; }
  .hero a { display: inline-block; padding: 12px 28px; background: #fff; color: #4A3CC0; border-radius: 8px; font-weight: 600; text-decoration: none; }
</style>
<div class="hero">
  <h1>{{headline}}</h1>
  <p>{{subhead}}</p>
  <a href="{{ctaHref}}">{{ctaText}}</a>
</div>
```

**Fields:** `headline` (text), `subhead` (text), `ctaText` (text), `ctaHref` (text).

***

## Recipe 2 — An FAQ accordion (no JavaScript)

Native `<details>`/`<summary>` gives you collapsible sections without any script — which keeps the component clean under the sandbox. Fields fill the questions and answers.

```html
<style>
  .faq { max-width: 640px; font-family: system-ui, -apple-system, sans-serif; }
  .faq details { border: 1px solid #e5e7eb; border-radius: 10px; padding: 14px 18px; margin-bottom: 10px; }
  .faq summary { font-weight: 600; cursor: pointer; color: #111827; }
  .faq p { margin: 10px 0 0 0; color: #374151; font-size: 0.9375rem; line-height: 1.6; }
</style>
<div class="faq">
  <details><summary>{{q1}}</summary><p>{{a1}}</p></details>
  <details><summary>{{q2}}</summary><p>{{a2}}</p></details>
  <details><summary>{{q3}}</summary><p>{{a3}}</p></details>
</div>
```

**Fields:** `q1`–`q3` and `a1`–`a3` (use `textarea` for the answers).

***

## Recipe 3 — A personalized account summary card

Prints live identity and billing data using **declarative bindings**. Declare two bindings on the component:

* `viewer` — kind **viewer**, no params.
* `account` — kind **currentAccount**, params `{ "accountId": {"$ctx":"account.id"} }` (the `$ctx` token binds it to the signed-in viewer's own account, filled server-side).

```html
<style>
  .acct { padding: 24px; border: 1px solid #e5e7eb; border-radius: 12px; background: #fff; font-family: system-ui, -apple-system, sans-serif; max-width: 420px; }
  .acct h3 { margin: 0 0 2px 0; font-size: 1.125rem; color: #111827; }
  .acct .sub { color: #6b7280; font-size: 0.8125rem; margin: 0 0 16px 0; }
  .acct .row { display: flex; justify-content: space-between; padding: 8px 0; border-top: 1px solid #f3f4f6; font-size: 0.875rem; }
  .acct .row .k { color: #6b7280; }
  .acct .row .v { color: #111827; font-weight: 600; }
</style>
<div class="acct">
  <h3>Welcome back, {{data.viewer.displayName}}</h3>
  <p class="sub">Account {{data.account.accountNumber}} · {{data.account.status}}</p>
  <div class="row"><span class="k">Balance</span><span class="v">{{data.account.balance}} {{data.account.currency}}</span></div>
  <div class="row"><span class="k">MRR</span><span class="v">{{data.account.mrr}} {{data.account.currency}}</span></div>
  <div class="row"><span class="k">Next bill</span><span class="v">{{data.account.nextBillingDate}}</span></div>
</div>
```

**Bindings:** `viewer` → `{ isAuthenticated, id, email, role, displayName, tenantName, … }`; `account` (currentAccount) → `{ accountNumber, name, status, balance, currency, mrr, nextBillingDate, … }`.

***

## Recipe 4 — A "most recent invoice" card

Declarative bindings can index into a **list** with a numbered path, so you can surface the first item without any script. Declare an `invoices` binding (kind **invoices**, params `{ "accountId": {"$ctx":"account.id"}, "filter": "open", "limit": 1 }`) and read `{{data.invoices.0.…}}` — the first invoice returned.

```html
<style>
  .inv { padding: 20px 24px; border: 1px solid #e5e7eb; border-radius: 12px; background: #fff; font-family: system-ui, -apple-system, sans-serif; max-width: 380px; }
  .inv .num { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: #6b7280; margin: 0; }
  .inv .amt { font-size: 1.75rem; font-weight: 700; color: #111827; margin: 4px 0 0 0; }
  .inv .meta { font-size: 0.8125rem; color: #374151; margin: 12px 0 0 0; }
  .inv .status { display: inline-block; margin-top: 8px; padding: 2px 10px; border-radius: 999px; background: #fef3c7; color: #92400e; font-size: 0.75rem; font-weight: 600; }
</style>
<div class="inv">
  <p class="num">Invoice {{data.invoices.0.invoiceNumber}}</p>
  <p class="amt">{{data.invoices.0.amount}} {{data.invoices.0.currency}}</p>
  <p class="meta">Issued {{data.invoices.0.invoiceDate}} · due {{data.invoices.0.dueDate}}</p>
  <span class="status">{{data.invoices.0.status}}</span>
</div>
```

**Binding:** `invoices` → `Array<{ id, invoiceNumber, invoiceDate, dueDate, amount, balance, currency, status, paidAt }>`. *(Indexed paths print one item; for a full, variable-length table, use the editor's **Data** panel rather than the template.)*

***

## Recipe 5 — A custom "Continue" button inside a journey

When a component runs as a **journey step** (and the journey hides its default chrome), the component can drive the flow itself through the `journey-actions` channel. Attach the handler with `addEventListener` in an inline `<script>` — **inline `onclick="…"` attributes are blocked by the sandbox**, but inline `<script>` blocks are allowed.

```html
<style>
  .choice button { margin: 6px; padding: 12px 20px; border: 1px solid #d1d5db; border-radius: 8px; background: #fff; font-weight: 600; cursor: pointer; font-family: system-ui, -apple-system, sans-serif; }
  .choice button.go { background: #5B4BD6; color: #fff; border-color: #5B4BD6; }
</style>
<div class="choice">
  <button data-plan="starter">Starter</button>
  <button data-plan="pro">Pro</button>
  <button class="go" id="continue">Continue</button>
</div>
<script>
  var chosen = "starter";
  document.querySelectorAll('[data-plan]').forEach(function (b) {
    b.addEventListener('click', function () { chosen = b.getAttribute('data-plan'); });
  });
  document.getElementById('continue').addEventListener('click', async function () {
    // Save the choice into the journey's shared context, then advance.
    await window.PC.call('journey-actions', 'setVariables', { selectedPlan: chosen });
    var r = await window.PC.call('journey-actions', 'advanceStep');
    if (!r.ok) console.log('Could not advance:', r.error); // e.g. "no-journey-host" off-journey
  });
</script>
```

You can also **read** the journey's context the same way — `await window.PC.call('journey-actions', 'getVariables')` resolves to `{ ok: true, result: { …variables } }` — to prefill the component from earlier steps.

***

## `window.PC` quick reference

`window.PC.call(channel, action, params)` returns a `Promise<{ ok, result?, error? }>`.

**`journey-actions`** *(only when the component runs inside a journey step; otherwise every action returns `{ ok:false, error:"no-journey-host" }`)*

| action                                     | does                                              |
| ------------------------------------------ | ------------------------------------------------- |
| `getVariables`                             | read the journey's shared context                 |
| `setVariables`                             | write string values into the context              |
| `advanceStep`                              | move to the next step (optionally passing values) |
| `goBack`                                   | return to the previous step                       |
| `markStepCompleted` / `resetStepCompleted` | toggle the step's completion                      |

**`data`** *(always available; the parent proxies to public endpoints since the iframe can't fetch)*

| action                 | does                                            |
| ---------------------- | ----------------------------------------------- |
| `product-catalog`      | the sellable catalog                            |
| `pricing-resolve`      | resolve live pricing for a selection            |
| `product-set-resolved` | resolve a product set                           |
| `get-context`          | the current page/journey context                |
| `navigate`             | navigate the parent (e.g. to a thank-you page)  |
| `hosted-pages`         | open the billing provider's hosted payment page |

Unknown channels or actions return `{ ok:false, error:"unknown-action" }`, and every input is validated — only the named fields are forwarded.

## Tips

* **Prefer `{{data}}` for printing, `window.PC.call` for doing.** Scalars and single items render cleanest as declarative bindings; interactivity and journey control go through the bridge.
* **No inline event handlers.** Attach listeners with `addEventListener` in an inline `<script>`; `onclick="…"` attributes are flagged by the security scanner.
* **No outbound `fetch`.** `connect-src 'none'` blocks it by design — reach data through a declared binding or a `data`-channel action instead.
* **Bind to the viewer's own account.** Use `{"$ctx":"account.id"}` for `accountId` so a customer can only ever read their own data.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.peakcommerce.com/developers/component-cookbook.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
