<!-- Generated from https://www.karbonkit.com/docs/integration/ when the site was built. Edit the page's source, not this file. -->

> Markdown version of [Integration Guide: Embed Any KarbonKit Widget | KarbonKit](https://www.karbonkit.com/docs/integration/).

Documentation

# KarbonKit integration guide

Everything needed to put a KarbonKit widget on a web page: the snippet for each widget, every setting with its type, default and allowed values, how sizing works, and working examples for plain HTML, React, Next.js and WordPress. If an attribute, parameter or setting is not on this page, KarbonKit does not support it. A plainer overview is at [/docs/](https://www.karbonkit.com/docs/), and this page is also available as Markdown at [/docs/integration.md](https://www.karbonkit.com/docs/integration.md).

## For AI coding assistants

-   **Snippets:** each widget’s section under [Widgets](#widgets) has a complete one. This page as Markdown: [https://www.karbonkit.com/docs/integration.md](https://www.karbonkit.com/docs/integration.md). All KarbonKit docs in one file: [https://www.karbonkit.com/llms-full.txt](https://www.karbonkit.com/llms-full.txt).
-   **Config ID:** the site owner gets one per widget from the dashboard after signing up at [https://www.karbonkit.com/signup](https://www.karbonkit.com/signup). If you have not been given one, leave `YOUR_CONFIG_ID` in place and tell the user where to get it. Never invent one.
-   **Use the script tag** unless the platform strips scripts or the site’s Content Security Policy forbids them; then use that widget’s plain iframe exactly as given.
-   **Do not** self-host, copy or bundle `embed.js` or any widget code: load it from `https://widgets.karbonkit.com/embed.js`. Do not add an `integrity` (SRI) hash to that tag: the file is updated in place at the same URL, so a pinned hash would block it after the next update.
-   **Do not** guess option names, `data-` attributes, URL parameters or widget types. The loader reads only `data-karbonkit` and `data-config-id`, and settings exist only in the dashboard. If the user wants a colour, company name or other setting, tell them which dashboard setting it is (the tables below name each one). A widget that is already embedded is changed from the dashboard (Your saved widgets, then Edit), not in the snippet.
-   **Do not** change an iframe’s `type`, `allow` list or the host, or call KarbonKit’s API from the page. There is no JavaScript API; the only message a widget sends is `karbonkit:resize`.

## Before you start: the config ID

Every widget is identified by a **config ID**. Snippets on this page use the placeholder `YOUR_CONFIG_ID`: replace it with the ID the dashboard gives you before you publish.

-   **Where it comes from.** Create a free account at [https://www.karbonkit.com/signup](https://www.karbonkit.com/signup) (or sign in at [https://www.karbonkit.com/dashboard](https://www.karbonkit.com/dashboard)), configure a widget on its tab, and copy its snippet. Copying saves the widget and puts its config ID in the snippet.
-   **One per widget, not per site.** Each widget you configure gets its own ID, and the widget type and settings are saved against it. The same ID works on any number of pages and sites; nothing ties it to a domain. Two different widgets need two IDs.
-   **What it looks like.** 22 characters of letters, digits, `-` and `_`. The loader rejects anything outside A–Z, a–z, 0–9, \_ and -, 8 to 64 characters.
-   **It is not a secret.** It sits in your page’s HTML, where anyone can read it. It carries no account access: it only selects which saved widget to show.
-   **Do not make one up.** With an ID the dashboard did not issue, the savings calculator and AR visualiser show an error instead of the widget, and the other widgets show their default settings.

## Two ways to embed

### Script tag (recommended)

The same two lines work for every widget. The widget type and settings come from the config ID.

HTML

```html
<script src="https://widgets.karbonkit.com/embed.js"></script>
<div data-karbonkit data-config-id="YOUR_CONFIG_ID"></div>
```

What `https://widgets.karbonkit.com/embed.js` does:

-   Finds every element with both `data-karbonkit` and `data-config-id`, and replaces its contents with an iframe of `https://widgets.karbonkit.com/widget-embed?cid=<config id>&parentOrigin=<your origin>`. It reads no other attributes.
-   The iframe is `width:100%`, starts at 600px high, then takes the height the widget reports (61–4999px), so it never needs a scrollbar. It loads lazily (`loading="lazy"`).
-   It delegates every permission any widget needs (`allow="camera; microphone; xr-spatial-tracking; fullscreen; accelerometer; gyroscope; magnetometer; clipboard-write"`) and sandboxes the iframe (`sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"`). The browser still asks the visitor before a widget uses the camera or microphone.
-   It watches the page for containers added later, so it works with single-page apps, site builders and tag managers. Include it once per page; one copy serves every widget on it.
-   It makes no network request of its own, sets no global variables and has no JavaScript API. Nothing on your page needs to talk to it.
-   A missing or malformed `data-config-id` shows “Widget misconfigured: missing or invalid config id.” in the container.
-   Older snippets, `<div data-heatpump-widget="YOUR_CONFIG_ID"></div>`, still work: the value is read as the config ID. The older `data-widget-type`, `data-user-tier`, `data-allowed-site` and `data-primary-color` attributes are ignored.

### Plain iframe

Use this where the platform strips `<script>` tags or your Content Security Policy does not allow a third-party script. Each widget has its own `type`, height and `allow` list (see [Widgets](#widgets)). The iframe stays at the height you set unless you add the [resize listener](#resizing). Copy the `allow` list exactly: a missing permission only fails when the visitor taps the button that needs it.

Every URL parameter other than `type` and `cid` is ignored for branding and text, and the type saved against a config ID wins over `type`. The parameters older snippets carry are listed at [/docs/#url-parameters](https://www.karbonkit.com/docs/#url-parameters); do not add them to new embeds.

## Widgets

Seven widgets, each with a complete snippet. The dashboard adds an optional credit link after each one, outside the widget; you can delete it. Every widget also shows a small “Powered by KarbonKit” link inside it.

### Savings calculator

Sizes a heat pump, solar PV and a battery for a UK home from its postcode, its EPC record or the household's bills, then estimates costs, savings and carbon. Live demo: [/tools/savings-calculator/](https://www.karbonkit.com/tools/savings-calculator/).

| Fact | Value |
| --- | --- |
| Widget type | `lct-calculator` |
| Config ID | Required. Without one the widget shows a "Sign up to embed this widget" message instead; with an ID the dashboard did not issue, an error |
| Script tag height | Starts at 600px and follows the widget's content |
| Iframe height | 900px (the dashboard's value). On mobile it can run past 1500px, so use the script tag or the resize listener |
| Iframe allow | `clipboard-write` |

Script tag (recommended)

```html
<script src="https://widgets.karbonkit.com/embed.js"></script>
<div data-karbonkit data-config-id="YOUR_CONFIG_ID"></div>
<!-- Optional: you can delete the credit link below -->
<a href="https://www.karbonkit.com/">Heat pump tools by KarbonKit</a>
```

Plain iframe

```html
<iframe
  src="https://widgets.karbonkit.com/widget-embed?type=lct-calculator&cid=YOUR_CONFIG_ID"
  width="100%"
  height="900"
  frameborder="0"
  style="border: none; border-radius: 8px;"
  allow="clipboard-write"
></iframe>
<!-- Optional: you can delete the credit link below -->
<a href="https://www.karbonkit.com/">Heat pump tools by KarbonKit</a>
```

Settings, chosen in the dashboard and saved against the config ID:

| Dashboard setting | Stored field | Type | Default | Allowed values | What it changes |
| --- | --- | --- | --- | --- | --- |
| Brand colour | `primary_color` | hex colour | `#2d6a4f` | #RRGGBB (six hex digits). Anything else is saved as #2d6a4f | Buttons, selected options and the footer link |
| Company name | `company_name` | string | `(empty)` | Up to 80 characters | Shown in the calculator header ("KarbonKit" when empty) |
| Technologies | `available_lcts` | array of strings | `["heatPump", "solarPV", "battery"]` | Any non-empty subset of heatPump, solarPV, battery. An empty list shows all three |  |
| Show nearby installers after results | `show_installer_finder` | boolean | `true` | true or false |  |
| Caveat banner | `caveat` | string | `(empty)` | Up to 240 characters, shown as an amber banner at the top |  |
| Enquiry collection (Leads tab) | `lead_capture` | object | `off` | See "Enquiry collection settings" below |  |

### AR heat pump visualiser

Places a true-size heat pump or hot water cylinder in the visitor's own space through their phone camera (WebXR on Android Chrome, AR Quick Look on iOS Safari). Desktop browsers get a 3D viewer. Live demo: [/tools/heat-pump-visualiser/](https://www.karbonkit.com/tools/heat-pump-visualiser/).

| Fact | Value |
| --- | --- |
| Widget type | `heat-pump-ar` |
| Config ID | Required. Without one the widget shows a "Sign up to embed this widget" message instead; with an ID the dashboard did not issue, an error |
| Script tag height | Starts at 600px and follows the widget's content |
| Iframe height | 500px (the dashboard's value) |
| Iframe allow | `camera; xr-spatial-tracking; accelerometer; gyroscope; magnetometer; fullscreen` |

Script tag (recommended)

```html
<script src="https://widgets.karbonkit.com/embed.js"></script>
<div data-karbonkit data-config-id="YOUR_CONFIG_ID"></div>
<!-- Optional: you can delete the credit link below -->
<a href="https://www.karbonkit.com/">Heat pump tools by KarbonKit</a>
```

Plain iframe

```html
<iframe
  src="https://widgets.karbonkit.com/widget-embed?type=heat-pump-ar&cid=YOUR_CONFIG_ID"
  width="100%"
  height="500"
  frameborder="0"
  style="border: none; border-radius: 8px;"
  allow="camera; xr-spatial-tracking; accelerometer; gyroscope; magnetometer; fullscreen"
  allowfullscreen
></iframe>
<!-- Optional: you can delete the credit link below -->
<a href="https://www.karbonkit.com/">Heat pump tools by KarbonKit</a>
```

Settings, chosen in the dashboard and saved against the config ID:

| Dashboard setting | Stored field | Type | Default | Allowed values | What it changes |
| --- | --- | --- | --- | --- | --- |
| Brand colour | `primary_color` | hex colour | `#2d6a4f` | #RRGGBB (six hex digits). Anything else is saved as #2d6a4f | The AR button and badges |
| Background colour | `secondary_color` | hex colour | `#fafaf9` | #RRGGBB (six hex digits). Anything else is saved as #fafaf9 |  |
| Model that loads first | `model_id` | integer | `7` | One of 7 (Compact 7 kW heat pump, the default), 16 (8.5 kW heat pump), 17 (Low-profile 8 kW heat pump), 11 (Large 12 kW heat pump), 10 (Hot water cylinder), 12 (Wall-mounted heat pump) |  |

### Sound simulator

Plays a real heat pump recording and measures its level in decibels with the phone's microphone, so a visitor can hear it from different distances. Live demo: [/tools/sound-simulator/](https://www.karbonkit.com/tools/sound-simulator/).

| Fact | Value |
| --- | --- |
| Widget type | `sound-simulator` |
| Config ID | Optional, but use one: without it the widget has default colours and no company name, and nothing can be set for it in the dashboard |
| Script tag height | Starts at 600px and follows the widget's content |
| Iframe height | 700px (the dashboard's value) |
| Iframe allow | `microphone` |

Script tag (recommended)

```html
<script src="https://widgets.karbonkit.com/embed.js"></script>
<div data-karbonkit data-config-id="YOUR_CONFIG_ID"></div>
<!-- Optional: you can delete the credit link below -->
<a href="https://www.karbonkit.com/">Heat pump tools by KarbonKit</a>
```

Plain iframe

```html
<iframe
  src="https://widgets.karbonkit.com/widget-embed?type=sound-simulator&cid=YOUR_CONFIG_ID"
  width="100%"
  height="700"
  frameborder="0"
  style="border: none; border-radius: 8px;"
  allow="microphone"
></iframe>
<!-- Optional: you can delete the credit link below -->
<a href="https://www.karbonkit.com/">Heat pump tools by KarbonKit</a>
```

Settings, chosen in the dashboard and saved against the config ID:

| Dashboard setting | Stored field | Type | Default | Allowed values | What it changes |
| --- | --- | --- | --- | --- | --- |
| Company name | `company_name` | string | `(empty)` | Up to 80 characters | Shown as "Provided by …" |
| Brand colour | `primary_color` | hex colour | `#2d6a4f` | #RRGGBB (six hex digits). Anything else is saved as #2d6a4f | Only the footer link; the widget keeps its own colours |

### 3D house tour

A 3D cutaway home with a complete heat pump system, solar panels and a home battery, and a 9-stop guided tour. Live demo: [/tools/house-tour/](https://www.karbonkit.com/tools/house-tour/).

| Fact | Value |
| --- | --- |
| Widget type | `house-tour` (older embeds may say `system-diagram`, which shows the same widget) |
| Config ID | Optional, but use one: without it the widget has default colours and no company name, and nothing can be set for it in the dashboard |
| Script tag height | Starts at 600px and follows the widget's content |
| Iframe height | 660px (the dashboard's value) |
| Iframe allow | `fullscreen` |

Script tag (recommended)

```html
<script src="https://widgets.karbonkit.com/embed.js"></script>
<div data-karbonkit data-config-id="YOUR_CONFIG_ID"></div>
<!-- Optional: you can delete the credit link below -->
<a href="https://www.karbonkit.com/">Heat pump tools by KarbonKit</a>
```

Plain iframe

```html
<iframe
  src="https://widgets.karbonkit.com/widget-embed?type=house-tour&cid=YOUR_CONFIG_ID"
  width="100%"
  height="660"
  frameborder="0"
  style="border: none; border-radius: 8px;"
  allow="fullscreen"
  allowfullscreen
></iframe>
<!-- Optional: you can delete the credit link below -->
<a href="https://www.karbonkit.com/">Heat pump tools by KarbonKit</a>
```

Settings, chosen in the dashboard and saved against the config ID:

| Dashboard setting | Stored field | Type | Default | Allowed values | What it changes |
| --- | --- | --- | --- | --- | --- |
| Brand colour | `primary_color` | hex colour | `#2d6a4f` | #RRGGBB (six hex digits). Anything else is saved as #2d6a4f | Accents, tour buttons and the footer link |
| Company name | `company_name` | string | `(empty)` | Up to 80 characters | Shown as "Provided by …" |

### Installer finder

Postcode search for nearby installers, from a directory built from public records, with a link to check each one on the MCS register. Live demo: [/tools/installer-finder/](https://www.karbonkit.com/tools/installer-finder/).

| Fact | Value |
| --- | --- |
| Widget type | `installer-finder` |
| Config ID | Optional, but use one: without it the widget has default colours and no company name, and nothing can be set for it in the dashboard |
| Script tag height | Starts at 600px and follows the widget's content |
| Iframe height | 700px (the dashboard's value) |
| Iframe allow | Nothing needed |

Script tag (recommended)

```html
<script src="https://widgets.karbonkit.com/embed.js"></script>
<div data-karbonkit data-config-id="YOUR_CONFIG_ID"></div>
<!-- Optional: you can delete the credit link below -->
<a href="https://www.karbonkit.com/">Heat pump tools by KarbonKit</a>
```

Plain iframe

```html
<iframe
  src="https://widgets.karbonkit.com/widget-embed?type=installer-finder&cid=YOUR_CONFIG_ID"
  width="100%"
  height="700"
  frameborder="0"
  style="border: none; border-radius: 8px;"
></iframe>
<!-- Optional: you can delete the credit link below -->
<a href="https://www.karbonkit.com/">Heat pump tools by KarbonKit</a>
```

Settings, chosen in the dashboard and saved against the config ID:

| Dashboard setting | Stored field | Type | Default | Allowed values | What it changes |
| --- | --- | --- | --- | --- | --- |
| Brand colour | `primary_color` | hex colour | `#2d6a4f` | #RRGGBB (six hex digits). Anything else is saved as #2d6a4f | Buttons, selected options and the footer link |
| Company name | `company_name` | string | `(empty)` | Up to 80 characters | Shown as "Provided by …" |
| Installer source | `installer_source` | string | `directory` | directory (the public directory), both (your list first, then the directory), curated (only your list; with no entries yet, the directory is shown) |  |
| Your suggested installers | `curated_installers` | array of objects | `[]` | Up to 25. Each needs a name (80 characters); postcode, telephone, website (reduced to https://host), email, technologies (heat\_pump, solar\_pv, battery) and a note (160 characters) are optional. Shown first, labelled "Suggested by" you |  |

### Grant eligibility finder

A postcode and four questions show which UK funding schemes a household may qualify for, each linked to its official page. Live demo: [/tools/grant-finder/](https://www.karbonkit.com/tools/grant-finder/).

| Fact | Value |
| --- | --- |
| Widget type | `grant-finder` |
| Config ID | Optional, but use one: without it the widget has default colours and no company name, and nothing can be set for it in the dashboard |
| Script tag height | Starts at 600px and follows the widget's content |
| Iframe height | 900px (the dashboard's value) |
| Iframe allow | Nothing needed |

Script tag (recommended)

```html
<script src="https://widgets.karbonkit.com/embed.js"></script>
<div data-karbonkit data-config-id="YOUR_CONFIG_ID"></div>
<!-- Optional: you can delete the credit link below -->
<a href="https://www.karbonkit.com/">Heat pump tools by KarbonKit</a>
```

Plain iframe

```html
<iframe
  src="https://widgets.karbonkit.com/widget-embed?type=grant-finder&cid=YOUR_CONFIG_ID"
  width="100%"
  height="900"
  frameborder="0"
  style="border: none; border-radius: 8px;"
></iframe>
<!-- Optional: you can delete the credit link below -->
<a href="https://www.karbonkit.com/">Heat pump tools by KarbonKit</a>
```

Settings, chosen in the dashboard and saved against the config ID:

| Dashboard setting | Stored field | Type | Default | Allowed values | What it changes |
| --- | --- | --- | --- | --- | --- |
| Brand colour | `primary_color` | hex colour | `#2d6a4f` | #RRGGBB (six hex digits). Anything else is saved as #2d6a4f | Buttons, selected options and the footer link |
| Company name | `company_name` | string | `(empty)` | Up to 80 characters | Shown as "Provided by …" |
| Link to the visitor's council | `grant_finder.show_council_link` | boolean | `false` | true or false |  |
| Your local schemes | `grant_finder.local_schemes` | array of objects | `[]` | Up to 10. Each needs a name (100 characters), an https link and a date to stop showing it (YYYY-MM-DD, at most 3 years ahead). Provider (80), headline (100), summary (400) and council areas (up to 40; none means every visitor) are optional. Labelled "Added by" you |  |

### Boiler escape plan

The carbon arithmetic of replacing a working gas boiler now versus waiting for it to fail, then a readiness plan with optional reminders. Live demo: [/tools/boiler-escape-plan/](https://www.karbonkit.com/tools/boiler-escape-plan/).

| Fact | Value |
| --- | --- |
| Widget type | `boiler-escape-plan` |
| Config ID | Optional, but use one: without it the widget has default colours and no company name, and nothing can be set for it in the dashboard |
| Script tag height | Starts at 600px and follows the widget's content |
| Iframe height | 1000px (the dashboard's value). On mobile it can run past 1500px, so use the script tag or the resize listener |
| Iframe allow | Nothing needed |

Script tag (recommended)

```html
<script src="https://widgets.karbonkit.com/embed.js"></script>
<div data-karbonkit data-config-id="YOUR_CONFIG_ID"></div>
<!-- Optional: you can delete the credit link below -->
<a href="https://www.karbonkit.com/">Heat pump tools by KarbonKit</a>
```

Plain iframe

```html
<iframe
  src="https://widgets.karbonkit.com/widget-embed?type=boiler-escape-plan&cid=YOUR_CONFIG_ID"
  width="100%"
  height="1000"
  frameborder="0"
  style="border: none; border-radius: 8px;"
></iframe>
<!-- Optional: you can delete the credit link below -->
<a href="https://www.karbonkit.com/">Heat pump tools by KarbonKit</a>
```

Settings, chosen in the dashboard and saved against the config ID:

| Dashboard setting | Stored field | Type | Default | Allowed values | What it changes |
| --- | --- | --- | --- | --- | --- |
| Company name | `company_name` | string | `(empty)` | Up to 80 characters | Shown as "Provided by …" |
| Brand colour | `primary_color` | hex colour | `#2d6a4f` | #RRGGBB (six hex digits). Anything else is saved as #2d6a4f | Only the enquiry form and the footer link; the widget keeps its own colours |
| Enquiry collection (Leads tab) | `lead_capture` | object | `off` | See "Enquiry collection settings" below |  |

### Enquiry collection settings

The savings calculator and the boiler escape plan can show a “get in touch” form after the visitor’s results, sending their details to your address and your dashboard’s Leads tab. It is off unless you switch it on for that widget in the Leads tab, and it can only be switched on there: no snippet or URL parameter can turn it on. Your own privacy notice needs to cover contacting the visitors who use it.

| Dashboard setting | Stored field (in lead\_capture) | Type | Default | Allowed values |
| --- | --- | --- | --- | --- |
| Collect enquiries | `enabled` | boolean | `false` | Stays false unless a valid notification address is set |
| Send enquiries to | `notify_email` | email address | `(empty)` | Required to switch collection on. Never sent to the visitor's browser |
| Your organisation's name | `owner_label` | string | `(empty)` | Up to 80 characters; names who the visitor agrees to hear from |
| Consent wording | `consent_text` | string | `(empty: standard wording)` | Up to 300 characters |
| Ask for a phone number | `ask_phone` | boolean | `false` | true or false |
| Ask what they are hoping to achieve | `ask_goal` | boolean | `false` | true or false |

### How settings behave

-   Settings are saved against the config ID when you copy the snippet. To change a widget that is already on your site, open its tab in the dashboard, choose it under **Your saved widgets**, click **Edit**, change the settings and click **Save changes**. The change shows on every page using that config ID at its next load, with no change to your HTML.
-   Changing settings and clicking Copy again, without editing a saved widget, saves a new widget with a new config ID. The old one keeps its old settings.
-   Nothing in the snippet can change a setting: the loader reads only the config ID, and an iframe with a dashboard config ID ignores other URL parameters.
-   Restricting a widget to your own domain is not available: the dashboard only saves an allowed domain for paid accounts, and there is no paid plan today.
-   Removing the “Powered by KarbonKit” link is set up by hand on request (hello@karbonkit.com), not in the dashboard.

## Height and resizing

Every widget posts its content height to the page that embeds it whenever it changes, as a `postMessage` of `{ type: 'karbonkit:resize', height: <pixels> }` from `https://widgets.karbonkit.com`.

-   **Script tag:** handled for you. Nothing to add.
-   **Plain iframe:** stays at its `height` attribute. Either set it tall enough for the smallest screen you care about (the calculator and boiler escape plan can pass 1500px on mobile), or add this listener once, anywhere on the page. It resizes every KarbonKit iframe on the page and ignores messages from anywhere else:

HTML

```html
<script>
  window.addEventListener('message', function (event) {
    if (event.origin !== 'https://widgets.karbonkit.com') return;
    if (!event.data || event.data.type !== 'karbonkit:resize') return;
    var frames = document.querySelectorAll('iframe[src^="https://widgets.karbonkit.com/"]');
    for (var i = 0; i < frames.length; i++) {
      if (frames[i].contentWindow === event.source) {
        frames[i].style.height = event.data.height + 'px';
      }
    }
  });
</script>
```

A plain iframe learns your page’s origin from the browser’s `Referer` header and sends its height only to that origin. A page served with `Referrer-Policy: no-referrer`, or an iframe with `referrerpolicy="no-referrer"`, gets no height messages: use the script tag there. Keep the `height` attribute on the iframe as the size it shows before the first message arrives.

## Examples

### Plain HTML

index.html

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>What could a heat pump save you?</title>
</head>
<body>
  <h1>What could a heat pump save you?</h1>

  <!-- KarbonKit widget. Replace YOUR_CONFIG_ID with the config ID from your dashboard. -->
  <div data-karbonkit data-config-id="YOUR_CONFIG_ID"></div>
  <script src="https://widgets.karbonkit.com/embed.js"></script>
</body>
</html>
```

Several widgets on one page need one loader:

HTML

```html
<div data-karbonkit data-config-id="YOUR_CONFIG_ID"></div>
<!-- ...the rest of your page... -->
<div data-karbonkit data-config-id="YOUR_OTHER_CONFIG_ID"></div>

<!-- One copy of the loader serves every widget on the page. -->
<script src="https://widgets.karbonkit.com/embed.js"></script>
```

### React

Add the loader from an effect rather than as a `<script>` element in JSX. The container is an empty `<div>`: React renders it, the loader fills it.

KarbonKitWidget.jsx

```jsx
// KarbonKitWidget.jsx
import { useEffect } from 'react';

const LOADER = 'https://widgets.karbonkit.com/embed.js';

export default function KarbonKitWidget({ configId }) {
  // Add the loader once per page. It also mounts containers React adds
  // later, so client-side navigation and remounts need nothing extra.
  useEffect(() => {
    if (document.querySelector(`script[src="${LOADER}"]`)) return;
    const script = document.createElement('script');
    script.src = LOADER;
    document.body.appendChild(script);
  }, []);

  // The loader fills each container once; key gives a new config ID a new one.
  return <div key={configId} data-karbonkit="" data-config-id={configId} />;
}
```

Using it

```jsx
import KarbonKitWidget from './KarbonKitWidget';

export default function HeatPumpPage() {
  return (
    <section>
      <h1>What could a heat pump save you?</h1>
      <KarbonKitWidget configId="YOUR_CONFIG_ID" />
    </section>
  );
}
```

Without the loader, as an iframe (take `type`, `height` and `allow` from the widget’s section above):

KarbonKitIframe.jsx

```jsx
// KarbonKitIframe.jsx – for sites whose Content Security Policy will not
// allow a third-party script. Take type, height and allow from the widget's
// section of this guide.
import { useEffect, useRef } from 'react';

const HOST = 'https://widgets.karbonkit.com';

export default function KarbonKitIframe({ type, configId, height, allow = '' }) {
  const ref = useRef(null);

  // Optional: follow the widget's height instead of staying at `height`.
  useEffect(() => {
    const onMessage = (event) => {
      const iframe = ref.current;
      if (!iframe || event.origin !== HOST || event.source !== iframe.contentWindow) return;
      if (event.data && event.data.type === 'karbonkit:resize') {
        iframe.style.height = `${event.data.height}px`;
      }
    };
    window.addEventListener('message', onMessage);
    return () => window.removeEventListener('message', onMessage);
  }, []);

  return (
    <iframe
      ref={ref}
      title="KarbonKit widget"
      src={`${HOST}/widget-embed?type=${type}&cid=${configId}`}
      width="100%"
      height={height}
      style={{ border: 'none', borderRadius: 8 }}
      allow={allow || undefined}
      allowFullScreen={allow.includes('fullscreen')}
    />
  );
}

// <KarbonKitIframe type="lct-calculator" configId="YOUR_CONFIG_ID" height={900} allow="clipboard-write" />
```

### Next.js

The same component, marked as a Client Component. Its effect runs after hydration, so the loader never alters markup React is still hydrating. Any page, including a Server Component, can render it.

components/KarbonKitWidget.jsx

```jsx
// components/KarbonKitWidget.jsx
'use client';

import { useEffect } from 'react';

const LOADER = 'https://widgets.karbonkit.com/embed.js';

export default function KarbonKitWidget({ configId }) {
  // Runs after hydration, so the loader never changes markup React is
  // still hydrating.
  useEffect(() => {
    if (document.querySelector(`script[src="${LOADER}"]`)) return;
    const script = document.createElement('script');
    script.src = LOADER;
    document.body.appendChild(script);
  }, []);

  return <div key={configId} data-karbonkit="" data-config-id={configId} />;
}
```

App Router

```jsx
// app/heat-pumps/page.jsx – a Server Component can render the widget
import KarbonKitWidget from '../../components/KarbonKitWidget';

export default function Page() {
  return (
    <main>
      <h1>What could a heat pump save you?</h1>
      <KarbonKitWidget configId="YOUR_CONFIG_ID" />
    </main>
  );
}
```

Pages Router

```jsx
// pages/heat-pumps.jsx – the Pages Router uses the same component
// ('use client' is ignored there and does no harm).
import KarbonKitWidget from '../components/KarbonKitWidget';

export default function HeatPumps() {
  return <KarbonKitWidget configId="YOUR_CONFIG_ID" />;
}
```

If your site sets a Content Security Policy (for example in `middleware.js` or `next.config.js`), add the sources under [Content Security Policy](#csp).

### WordPress (block editor)

1.  Edit the page, click **+** to add a block, and choose **Custom HTML** (not “Code”, which shows the markup as text).
2.  Paste the widget’s script-tag snippet from its section above, with your config ID in it.
3.  Use the block’s **Preview** tab or preview the page, then publish.

Custom HTML block

```html
<script src="https://widgets.karbonkit.com/embed.js"></script>
<div data-karbonkit data-config-id="YOUR_CONFIG_ID"></div>
<!-- Optional: you can delete the credit link below -->
<a href="https://www.karbonkit.com/">Heat pump tools by KarbonKit</a>
```

-   WordPress removes `<script>` and `<iframe>` tags when the post is saved by a user without the `unfiltered_html` capability (on a standard install only Administrators and Editors have it; on multisite only Super Admins). If the block comes back empty, ask an administrator to add it.
-   If scripts are stripped but iframes are allowed, paste the widget’s plain-iframe snippet instead. On WordPress.com, iframes need the Business plan or above.
-   If a performance plugin delays or combines JavaScript, exclude `https://widgets.karbonkit.com/embed.js` from it, or the widget appears late or not at all.

Other platforms (Squarespace, Wix, Webflow, Shopify, Ghost) are covered at [/docs/](https://www.karbonkit.com/docs/).

## Troubleshooting

### Content Security Policy

If your page sends a Content Security Policy, allow the widget host:

CSP directives

```
script-src https://widgets.karbonkit.com;   # script-tag embeds only
frame-src  https://widgets.karbonkit.com;   # both embed methods
```

-   Add these to your existing sources; do not replace them. With no `frame-src`, browsers fall back to `child-src`, then `default-src`.
-   Nothing else is needed: the loader makes no network request, and everything the widget loads is governed by the widget page’s own policy, not yours.
-   If your policy uses nonces or hashes for scripts, the loader’s `<script>` tag needs your nonce like any other script, or use the plain iframe.
-   A `Permissions-Policy` header on your page that disables `camera`, `microphone` or `xr-spatial-tracking` (for example `camera=()`) overrides the iframe’s `allow` list, and the AR visualiser or sound simulator cannot use them.

### Iframe height

-   **Cut off, or an inner scrollbar:** a plain iframe is fixed at its `height`. Add the [resize listener](#resizing) or switch to the script tag. Mobile Safari does not reliably scroll inside an iframe, so a taller fixed height is the weaker fix.
-   **Script tag stuck at 600px:** the loader only accepts height messages from the host it was loaded from. Load it from `https://widgets.karbonkit.com/embed.js` exactly; a copy served from anywhere else never resizes.
-   **A parent with a fixed height or `overflow: hidden`** clips the widget whatever its own height. Let the container grow.

### Cookies and consent

-   Widgets set no cookies, store nothing in the visitor’s browser and carry no advertising or third-party analytics trackers; KarbonKit records usage events itself, with a truncated IP address. The loader stores nothing either. Details, including what the widget sends to KarbonKit and to third parties such as postcodes.io, and a section for a data protection officer, are at [/privacy/](https://www.karbonkit.com/privacy/).
-   If your consent tool blocks third-party scripts or iframes until the visitor accepts, the widget stays blank until then. Whether to exempt it is your decision, based on what the privacy page says it processes.
-   Enquiry collection, if you switch it on, asks for contact details with the visitor’s explicit consent to hear from you. For those details you are the data controller, and your privacy notice needs to say so.

### Mixed content and HTTPS

-   Always use `https://widgets.karbonkit.com` URLs as given, with `https://`. On an HTTPS page, browsers block an `http://` script or iframe as mixed content.
-   On a plain `http://` page the widgets load, but the AR visualiser’s camera view and the sound simulator’s microphone meter do not work: browsers only allow those in a secure context, and an iframe is only secure if every page above it is. Serve the page over HTTPS (`http://localhost` counts as secure during development).
-   Snippets pointing at `http://localhost:3000` come from a development copy of the dashboard. Never publish them.

### What the widget’s messages mean

| Message | Meaning | Fix |
| --- | --- | --- |
| Widget misconfigured: missing or invalid config id. | The loader found a container whose data-config-id is empty or malformed. | Paste the config ID exactly as the dashboard gives it. |
| Widget not found. | No widget has this config ID (mistyped, or the widget was deleted). | Copy a fresh snippet from the dashboard. |
| Could not reach KarbonKit. Please try again in a moment. | The widget could not load its settings from KarbonKit’s API. | Reload. If it persists, contact hello@karbonkit.com. |
| This widget is not authorised for this site. | The widget has an allowed-domain list and your page is not on it. Only widgets with a domain list can show this. | Contact hello@karbonkit.com. |
| Sign up to embed this widget | A savings calculator or AR visualiser iframe has no config ID. | Add your config ID as cid=, or use the script tag. |
| Replace YOUR\_CONFIG\_ID in the embed code with your widget’s config ID… | The snippet was published with the placeholder still in it. | Put the config ID from the dashboard in its place. |
| This embed code does not carry a valid config ID. | A savings calculator or AR visualiser was given an ID the dashboard did not issue. | Copy a fresh snippet from the dashboard. |

## Not supported

So that nobody spends time trying them:

-   Self-hosting `embed.js` or the widget code. The loader points its iframe at the host it was loaded from, so a copy on your server tries to load the widget from your server.
-   Setting options in the snippet, through `data-` attributes, URL parameters or JavaScript. Settings exist only in the dashboard.
-   A JavaScript API, callbacks or events other than `karbonkit:resize`.
-   Styling the inside of the widget with your own CSS. It runs in an iframe on another origin; brand colour and company name are the supported controls.
-   Languages other than English, or audiences outside the UK.
