> 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/custom-components.md).

# Building Custom Components

When the built-in blocks don't cover a layout you need, you build your own **custom component** — a reusable block that becomes a first-class element in the page editor and the portals. You author components in **Content → Component Library** (and you can have Mango generate one for you — see *Meet Mango* in the help docs). This page is the **developer reference**: the code format, how fields work, how to bind live commerce data, and what the sandbox does and doesn't allow.

For the click-by-click editor walkthrough, see *Creating a Component* in the product docs. This page focuses on the code.

## The component model

A code-type component is plain **HTML + `<style>`** with **`{{field}}`** placeholders. There's no build step and no framework — what you write is what renders. At render time PeakCommerce:

* substitutes each `{{field}}` with the field's value,
* substitutes each `{{data.<name>}}` with the result of a declared data binding,
* **HTML-escapes every substituted value** (so a value containing `<script>` can never break out of the markup), and
* renders the result inside a **sandboxed iframe**.

Two rules follow from that pipeline:

* **Templating is pure substitution — there is no expression evaluation inside `{{ }}`.** A placeholder is a key or a dotted path, not JavaScript.
* **Unknown placeholders are left untouched.** If you write `{{titel}}` and never declared a `titel` field, the literal `{{titel}}` stays in the output — so you can spot the typo instead of silently rendering nothing (or, worse, leaking unrelated data).

## Fields — the editable props

Fields are the properties a page author can edit when they place your component. Each field has a `name`, a `type`, a `label`, a `defaultValue`, and (for `select`) comma-separated `options`:

| Field type | Use it for                                                 |
| ---------- | ---------------------------------------------------------- |
| `text`     | a single-line string                                       |
| `textarea` | a multi-line string                                        |
| `number`   | a numeric value                                            |
| `select`   | a fixed set of choices (`options: "info,success,warning"`) |
| `radio`    | a single choice from a small set                           |

Reference a field in your code as `{{fieldName}}`. The field's `name` is the token; the `label` is only what the author sees in the editor.

## Example: a pricing card

A self-contained component with six text fields:

```html
<style>
  .pricing-card { border: 1px solid #e5e7eb; border-radius: 12px; padding: 32px 24px; background: #fff; font-family: system-ui, -apple-system, sans-serif; text-align: center; max-width: 340px; }
  .pricing-card .tier-name { font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: #2563eb; margin: 0 0 8px 0; }
  .pricing-card .price { font-size: 2.5rem; font-weight: 700; color: #111827; margin: 0; line-height: 1.1; }
  .pricing-card .price span { font-size: 1rem; font-weight: 400; color: #6b7280; }
  .pricing-card .divider { height: 1px; background: #e5e7eb; margin: 20px 0; }
  .pricing-card .features { list-style: none; padding: 0; margin: 0 0 24px 0; text-align: left; }
  .pricing-card .features li { padding: 6px 0; font-size: 0.875rem; color: #374151; display: flex; align-items: center; gap: 8px; }
  .pricing-card .features li::before { content: "\2713"; color: #22c55e; font-weight: 700; }
  .pricing-card .cta-btn { display: inline-block; width: 100%; padding: 10px 24px; background: #2563eb; color: #fff; border: none; border-radius: 8px; font-size: 0.875rem; font-weight: 600; text-decoration: none; }
</style>
<div class="pricing-card">
  <p class="tier-name">{{tierName}}</p>
  <p class="price">{{price}} <span>/mo</span></p>
  <div class="divider"></div>
  <ul class="features"><li>{{feature1}}</li><li>{{feature2}}</li><li>{{feature3}}</li></ul>
  <a class="cta-btn" href="#">{{ctaText}}</a>
</div>
```

Its fields:

| name                                 | type | default                                                    |
| ------------------------------------ | ---- | ---------------------------------------------------------- |
| `tierName`                           | text | Professional                                               |
| `price`                              | text | $49                                                        |
| `feature1` / `feature2` / `feature3` | text | Unlimited projects / Priority support / Advanced analytics |
| `ctaText`                            | text | Get Started                                                |

## Example: an alert banner (a `select` driving a CSS class)

A field value can drive layout, not just text — here `{{alertType}}` is interpolated into a class name so the same component renders three styles:

```html
<style>
  .alert-banner { display: flex; align-items: center; gap: 12px; padding: 14px 20px; border-radius: 10px; font-family: system-ui, -apple-system, sans-serif; }
  .alert-banner.info { background: #eff6ff; border: 1px solid #bfdbfe; color: #1e40af; }
  .alert-banner.success { background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534; }
  .alert-banner.warning { background: #fffbeb; border: 1px solid #fde68a; color: #92400e; }
  .alert-banner .icon { font-size: 1.25rem; flex-shrink: 0; }
  .alert-banner .alert-title { font-size: 0.875rem; font-weight: 600; margin: 0 0 2px 0; }
  .alert-banner .alert-message { font-size: 0.8125rem; margin: 0; opacity: 0.85; }
</style>
<div class="alert-banner {{alertType}}">
  <span class="icon">{{icon}}</span>
  <div class="alert-content">
    <p class="alert-title">{{title}}</p>
    <p class="alert-message">{{message}}</p>
  </div>
</div>
```

Field: `alertType` — `select`, options `info,success,warning`, default `info` (plus `icon`, `title`, and `message` text fields).

## Binding to live commerce data

Fields are author-set and static. To render **live** data — the signed-in customer's name, their account balance, their invoices — declare a **data binding**. A binding has a `name` (yours), a `kind` (a platform-provided source), and optional `params`. You then reference it in code as **`{{data.<name>}}`**, or drill into the payload with a dotted path **`{{data.<name>.field}}`**.

Bindings are resolved **server-side**, so a component never holds tokens or permissions, the viewer's identity is supplied by the platform (not trusted from the page), and every resolution is **scope-checked, rate-limited, and audited**.

### Built-in binding kinds

| kind             | returns                           | auth                             |
| ---------------- | --------------------------------- | -------------------------------- |
| `viewer`         | the current visitor's identity    | public (works for anonymous too) |
| `currentAccount` | the billing account detail        | signed-in + scope check          |
| `invoices`       | an account's invoices             | signed-in + scope check          |
| `payments`       | an account's payments             | signed-in + scope check          |
| `subscriptions`  | an account's subscriptions        | signed-in + scope check          |
| `usage`          | usage drawdown for a subscription | signed-in + scope check          |
| `productCatalog` | sellable products/plans           | per configuration                |

The account-scoped kinds take an `accountId` param. Bind it to *the current viewer's own account* by passing the context token `{"$ctx":"account.id"}` rather than a hard-coded id — the resolver fills it from the authenticated viewer, so a customer can only ever read their own data.

### Binding result shapes

* **`viewer`** → `{ isAuthenticated, id, email, role, portalType, displayName, tenantId, tenantName }` (identity only — never permissions or tokens).
* **`currentAccount`** → `{ id, accountNumber, name, status, balance, currency, locale, mrr, createdDate, nextBillingDate, billToContact, soldToContact }`.

### Example: a personalized account banner

Declare two bindings — `viewer` (kind `viewer`, no params) and `account` (kind `currentAccount`, params `{ "accountId": {"$ctx":"account.id"} }`) — then read them in code:

```html
<style>
  .acct-banner { padding: 20px 24px; border-radius: 12px; background: #f5f4fd; font-family: system-ui, -apple-system, sans-serif; }
  .acct-banner h3 { margin: 0 0 4px 0; font-size: 1.125rem; color: #111827; }
  .acct-banner .muted { color: #6b7280; font-size: 0.875rem; margin: 0; }
  .acct-banner .balance { font-size: 1.5rem; font-weight: 700; color: #111827; margin: 12px 0 0 0; }
</style>
<div class="acct-banner">
  <h3>Welcome back, {{data.viewer.displayName}}</h3>
  <p class="muted">Account {{data.account.accountNumber}} · {{data.account.status}}</p>
  <p class="balance">{{data.account.balance}} {{data.account.currency}}</p>
  <p class="muted">Next bill: {{data.account.nextBillingDate}}</p>
</div>
```

Because `{{data.…}}` substitution resolves a single value, it's ideal for scalar fields like a name or a balance. For **lists** (an invoices table, a subscriptions grid), wire the binding through the editor's **Data** panel, which connects your component to the live payload — open the Data tab in the component editor to set this up rather than constructing the call by hand.

## What you can — and can't — put in a component

Components render in a **sandboxed iframe** under a Content-Security-Policy, and every saved or AI-generated component is run through a **security scanner**. Keep components **self-contained**:

* ✅ Inline `<style>` and HTML markup — the normal way to build a component.
* ✅ Live data via **declared data bindings** — the supported way to reach commerce data.
* ⚠️ **External scripts, stylesheets, fonts, images, iframes**, and **outbound network calls** (`fetch`/`XHR` to other origins) are flagged by the scanner. Bundle what you need and use bindings for data instead of fetching.
* ⚠️ **Inline event handlers** (`onclick="…"`), `javascript:` URLs, `<base>`/`<meta refresh>`, and similar are flagged. Avoid them.
* 🔒 Every interpolated value is **HTML-escaped**, so you can't inject raw HTML through a field or binding value — by design.

Validation runs when you **save**, and **enforcement is the default**: a component whose code contains a blocking construct from the lists above is rejected on save, with every finding listed so you can fix each one — so a component that saves cleanly stays clean. A report-only mode exists only as a temporary, platform-level fallback operated by PeakCommerce; it is not a setting you can choose per tenant.

## Where a component can appear

A component shows up only where you allow it. Set these on the component:

* **Contexts** — which editors / page types it's available in: Storefront, Checkout, Customer Portal, Partner Portal, CSR Portal, or Payment Pages. A component missing from a page almost always has the wrong contexts.
* **Audience roles** and **required permissions** — narrow who can see or use it.
* **Status** — it must be **Active** to appear as a block.

## Built-in components to start from

Four code components ship seeded in the library — open any of them in the component editor to see complete, working code:

* **Pricing Card** — a pricing tier with feature list and CTA (shown above).
* **Alert Banner** — an info/success/warning notice (shown above).
* **Metric Dashboard** — a responsive grid of KPI cards with up/down trend styling.
* **Testimonial Block** — a quote with avatar initials and attribution (a `textarea` quote plus text fields).

## Building one

1. In **Content → Component Library**, click **New Component** (or **Create with Mango** to generate a first draft from a description).
2. Paste or write your HTML + `<style>` in the **Code** view.
3. Define your **fields** so authors can edit the right values, and reference them as `{{name}}`.
4. Declare any **data bindings** and read them as `{{data.<name>.field}}`.
5. Set the **contexts** it should appear in, mark it **Active**, and save.

It's now a block in the page editor everywhere its context matches.


---

# 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/custom-components.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.
