# Embed the Kreativsimulator via `<iframe>`

> Developer guide: how to embed the Kreativsimulator in your website via iframe – with a live demo and full remote control via postMessage (your own products & creatives).
>
> Human-readable version: https://virtualcampaign.dev/en/embed (German: https://virtualcampaign.dev/embed)
> Live sandbox: https://demo.kreativsimulator.com/
> This file is auto-generated from the live docs on every build — it is always current.

Embed the Kreativsimulator into your own website in seconds – and optionally drive it entirely from the outside: your own product picker, your own creatives, your own UI. This guide walks every step with copy-paste code and a live demo to try.

## Step 1: Get your domains whitelisted

For security reasons the browser only allows embedding **from domains we have whitelisted in advance** (`Content-Security-Policy: frame-ancestors`).

Before embedding, send us **all domains** the iframe should appear on. We add them to your tenant configuration. Wildcards are supported.

For example:

- `https://www.your-agency.com`
- `https://campaign.your-agency.com`
- `https://*.your-agency.com  (wildcard)`

**Important:** Without whitelisting the iframe stays blank (white area). The live demo below also only loads if the embedding domain is whitelisted – otherwise use the “Open in new tab” link.

## Step 2: Embed code

A single `<iframe>` tag is enough. No API key, no extra configuration on your side – authentication runs entirely server-side.

```html
<iframe
  src="https://demo.kreativsimulator.com/"
  title="Kreativsimulator"
  style="width: 100%; height: 720px; border: 0;"
  allow="fullscreen"
  loading="lazy"
></iframe>
```

**Embed URL:** `https://<your-tenant>.kreativsimulator.com/`

The tenant is derived from the subdomain. For the public sandbox use `demo.kreativsimulator.com`.

Language and start product can optionally be preset via query parameters – see the next section.

## Step 3: URL parameters

Optionally append these query parameters to the embed URL (start with `?`, chain with `&`):

| Parameter | Values | Effect | Default |
| --- | --- | --- | --- |
| `lang` | `de · en · fr · it` | UI language | Tenant → browser → de |
| `product` | Product **code** (`ec5472c2`) | Loads this product directly (deep link). Unknown code → first product. | First product |
| `header` | `0 · 1` | Show/hide the header (product selector) | `1` (visible) |
| `sidebar` | `0 · 1` | Show/hide the sidebar (incl. mobile drawer + hamburger) | `1` (visible) |
| `playback` | `0 · 1` | Show/hide the playback/transport buttons in the bar | `1` (visible) |
| `timeline` | `0 · 1` | Show/hide the timeline (scrubbing bar) | `1` (visible) |
| `export` | `0 · 1` | Show/hide the “Export creation” button | `1` (visible) |
| `effects` | `0 · 1` | Show/hide the effect dropdown (photo products only) – e.g. for your own picker | `1` (visible) |
| `fullscreen` | `0 · 1` | Fullscreen icon: `0`=never, `1`=always (incl. desktop); omitted = mobile / in fullscreen only | adaptive |
| `embed` | `minimal · kiosk` | Shorthand: hides header, sidebar and export (playback bar stays) | – |
| `theme` | URL to a CSS file | Loads a custom theme (CSS variables) that overrides the tenant theme | – |
| `bg` | **any CSS color**, `transparent` **or** image URL | Background of the area **around** the canvas (letterbox/frame), not the video | Theme background |
| `stage` | `<width>x<height>` (`1280x720`) | Fixed display pixel size of the canvas instead of fluid width | fluid (100%) |
| `placeholderBg` | any CSS color | Background of the **placeholder box** (loader without a creative) – direct shortcut, no `?theme=` file | Theme/default (`#ffffff`) |
| `placeholderText` | any CSS color | Color of the letter inside the placeholder box | Theme/default (`#000000`) |
| `placeholderBorder` | any CSS color | Border color of the placeholder box | Theme/default (`#000000`) |
| `watermark` | `0 · 1` | `1` = every exported MP4 gets the “Vorschau” (preview) watermark (dialog **and** postMessage export). **Additive only** – on the demo tenant it is always on and cannot be disabled. | `0` (demo tenant only) |

- **No custom branding inside the UI:** the simulator shows **no logo and no title** – the header contains only the product selector (and the menu icon). Branding comes from your page around the iframe. **The exported MP4 keeps the tenant's logo** composited top-left; this is intentional and cannot be disabled.
- **Boolean parameters** accept `0`/`1` (also `true`/`false`, `yes`/`no`). `embed=kiosk` is evaluated first – individual parameters override it afterwards (e.g. `?embed=kiosk&header=1` still shows the header).
- **The visibility switches** (`header`/`sidebar`/`playback`/`timeline`/`export`/`effects`) can also be toggled **at runtime** via `setChrome` (see API). Playback, export and effect selection always work via postMessage – regardless of whether the matching controls are visible. So you can build a fully custom UI (your own effect picker via `setEffect`, hide the built-in dropdown with `effects=0`).
- **theme CSS:** loaded as an extra `<link>` **after** the tenant theme, overriding its CSS variables (e.g. `--vc-accent`, `--vc-bg`). CSS cannot run JavaScript; the file may be cross-origin (**no CORS needed**). Values for `?theme=`/`?bg=` must be URL-encoded (e.g. `%23` for `#`).
- **`bg=transparent`:** the **parent page shows through** the area around the canvas (best with `?embed=kiosk`). The video canvas itself always stays opaque – transparent only affects the frame/letterbox edges.
- **Placeholder colors directly in the URL** (`placeholderBg`/`placeholderText`/`placeholderBorder`): brands just the **placeholder box** (the generated letter motif shown until a creative is set) – a **shortcut with no `?theme=` file**. Accepts any CSS color (hex `%23ff5a10`, `rgb(...)`, `hsl(...)`, names like `white`) and overrides the tenant theme **and** `?theme=`; only the params you set override. *Tip:* don't set `placeholderBg` equal to `placeholderText` (invisible letter). For more branding (accent, buttons, spinner …) keep using `?theme=`.

### Examples

```text
https://demo.kreativsimulator.com/                          # default
https://demo.kreativsimulator.com/?lang=en                  # English UI
https://demo.kreativsimulator.com/?product=ec5472c2         # open a product
https://demo.kreativsimulator.com/?product=ec5472c2&lang=fr # language + product
https://demo.kreativsimulator.com/?embed=kiosk              # canvas + playback bar only
https://demo.kreativsimulator.com/?header=0&sidebar=0       # header + sidebar off
https://demo.kreativsimulator.com/?timeline=0&playback=0    # timeline + transport off
https://demo.kreativsimulator.com/?effects=0                # effect dropdown off (own picker)
https://demo.kreativsimulator.com/?fullscreen=1             # fullscreen icon also on desktop
https://demo.kreativsimulator.com/?watermark=1              # every export gets the preview watermark
https://demo.kreativsimulator.com/?stage=1280x720           # fixed 1280×720 canvas
https://demo.kreativsimulator.com/?bg=%23000000             # black letterbox area
https://demo.kreativsimulator.com/?embed=kiosk&bg=transparent  # parent page shows through
https://demo.kreativsimulator.com/?theme=https%3A%2F%2Fcdn.kunde.de%2Fks-theme.css

# Placeholder colors directly (no theme file) — encode # as %23:
https://demo.kreativsimulator.com/?placeholderBg=%23ff5a10&placeholderText=%23ffffff&placeholderBorder=%23ffffff
# … or with rgb()/color names (no %23 needed):
https://demo.kreativsimulator.com/?placeholderBg=rgb(255,90,16)&placeholderText=white&placeholderBorder=white
```

## Step 4: Embed responsively

In **embed mode** (chrome off, e.g. `?embed=kiosk` or `?sidebar=0`) the canvas **fills the iframe edge-to-edge** and stays **centered** – no left offset, no 1280px cap. So just drop the iframe with `width:100%; height:100%` into any sized parent container (flexbox/grid too). If its aspect ratio differs from 16:9 you get a **symmetric** letterbox (color via `?bg=`).

Recommended recipe – the iframe fills its flex/grid container:

```html
<!-- Any parent container; the iframe fills it completely. -->
<div style="display:flex; width:100%; height:480px;">
  <iframe
    src="https://demo.kreativsimulator.com/?embed=kiosk"
    title="Kreativsimulator"
    style="flex:1; border:0;"
    allow="fullscreen"
    loading="lazy"
  ></iframe>
</div>
```

For an exact aspect ratio without letterboxing, give the wrapper `aspect-ratio: 16 / 9`:

```html
<!-- Exact 16:9, no letterbox: constrain the wrapper. -->
<div style="aspect-ratio: 16 / 9; width: 100%;">
  <iframe
    src="https://demo.kreativsimulator.com/?embed=kiosk"
    title="Kreativsimulator"
    style="width: 100%; height: 100%; border: 0;"
    allow="fullscreen"
    loading="lazy"
  ></iframe>
</div>
```

With the sidebar visible (standalone view without `?embed`/`?sidebar=0`) the canvas stays left-aligned (max 1280px) – the fill/center logic deliberately applies only in embed mode.

## Step 5 — Core use case: Just the canvas – drive everything yourself

The most common case: show **only the WebGL canvas** and build the entire UI yourself. Hide header, sidebar and buttons – via URL with `?embed=kiosk` (header + sidebar + export off, playback bar stays) or selectively with `?header=0&sidebar=0&playback=0&export=0`. At runtime the same works via `setChrome`.

The key point: **playback and export always work via postMessage** – regardless of whether the built-in buttons are visible. So your own buttons drive the iframe; for a custom progress bar, listen to the `playbackState` event.

**Handy without the sidebar:** when the sidebar is hidden, clicking a placeholder directly on the canvas opens the file dialog for exactly that placeholder – so end users can upload a creative even without your own upload UI.

`embed=kiosk` hides the built-in chrome – your buttons send `play`/`pause`/`seek`/`export`. Try it in the live demo below with the “Chrome” toggles and the custom playback bar.

Canvas in kiosk mode + your own playback controls, kept in sync via the `playbackState` event:

```html
<!-- Canvas only — your own playback UI drives it from the outside -->
<iframe id="ks" src="https://demo.kreativsimulator.com/?embed=kiosk"
        style="width:100%;height:720px;border:0" allow="fullscreen"></iframe>

<button id="play">▶︎</button>
<button id="pause">⏸</button>
<button id="stop">⏹</button>
<button id="back">⏮</button>
<button id="fwd">⏭</button>
<input id="scrub" type="range" min="0" value="0" />
<button id="export">Export</button>
<button id="fs">⛶ Fullscreen</button>

<script>
  const iframe = document.getElementById("ks");
  const send = (msg) => iframe.contentWindow.postMessage(msg, "*");
  const P = "kreativsimulator:";

  play.onclick  = () => send({ type: P + "play" });
  pause.onclick = () => send({ type: P + "pause" });
  stop.onclick  = () => send({ type: P + "stop" });
  back.onclick  = () => send({ type: P + "stepBackward" });
  fwd.onclick   = () => send({ type: P + "stepForward" });
  scrub.oninput = (e) => send({ type: P + "seek", frame: Number(e.target.value) });
  // Direct export at 1280×720 (no dialog):
  export.onclick = () =>
    send({ type: P + "export", filename: "creation", width: 1280, height: 720 });
  // Fullscreen ON reliably via requestFullscreen on the iframe (the user gesture
  // is here, on the click). OFF/toggle could also go via the postMessage event.
  fs.onclick = () => {
    if (document.fullscreenElement) document.exitFullscreen();
    else iframe.requestFullscreen();   // iframe needs allow="fullscreen"
  };

  window.addEventListener("message", (e) => {
    const m = e.data;
    if (!m || m.source !== "kreativsimulator") return;
    if (m.type === "playbackState") {
      scrub.max = m.frameCount - 1;
      if (document.activeElement !== scrub) scrub.value = m.frame; // don't fight the drag
      play.disabled = m.playing;
      pause.disabled = !m.playing;
    }
    if (m.type === "fullscreenChanged") {
      fs.textContent = m.fullscreen ? "⛶ Exit fullscreen" : "⛶ Fullscreen";
    }
  });

  // Ask for the current state once (e.g. after load):
  // send({ type: P + "getState" });
</script>
```

## Step 6 — Custom design: Your own design via CSS (theme & bg)

The iframe adopts your branding through CSS variables. `?theme=` loads a custom CSS file **after** the tenant theme and overrides its variables (e.g. `--vc-accent`, `--vc-bg`). `?bg=` sets the letterbox area **around** the canvas (color or image URL), `?stage=` a fixed canvas pixel size.

### Embed with theme + background

```html
<!-- Your own theme + a black letterbox around the canvas -->
<iframe
  src="https://demo.kreativsimulator.com/?embed=kiosk&theme=https%3A%2F%2Fcdn.kunde.de%2Fks-theme.css&bg=%23000000"
  title="Kreativsimulator"
  style="width: 100%; height: 720px; border: 0;"
  allow="fullscreen"
  loading="lazy"
></iframe>
```

### Theme template (all overridable CSS variables)

```css
/* =============================================================================
   Kreativsimulator – Theme-Vorlage (CSS Custom Properties)
   =============================================================================

   So passt du das Aussehen des eingebetteten Kreativsimulators an deine Marke an.

   VERWENDUNG
   ----------
   1. Diese Datei kopieren, Werte unten anpassen, auf einem öffentlich
      erreichbaren HTTPS-URL ablegen (z. B. https://cdn.deine-agentur.de/ks-theme.css).
   2. Den URL per `?theme=` an die iframe-Einbettung hängen – URL-kodiert:

      <iframe src="https://demo.kreativsimulator.com/?theme=https%3A%2F%2Fcdn.deine-agentur.de%2Fks-theme.css"
              style="width:100%;height:720px;border:0" allow="fullscreen"></iframe>

   HINWEISE
   --------
   • Die Datei wird als zusätzliches <link> NACH dem Mandanten-Theme geladen und
     überschreibt dessen Werte (CSS-Kaskade). Cross-Origin ist erlaubt (kein CORS
     nötig) – CSS kann kein JavaScript ausführen.
   • Du musst NICHT alle Variablen setzen. Lösche einfach die Zeilen, die du nicht
     ändern willst – dann gilt der jeweilige Standard des Simulators/Mandanten.
   • Farben: jede gültige CSS-Farbe (#hex, rgb(), hsl(), benannt).
   • Den Hintergrund UM den Canvas (Letterbox) steuerst du besser per
     `?bg=<farbe|transparent|url>` – nicht über `--vc-bg` (das ist auch der
     Canvas-Hintergrund während des Ladens).
   ========================================================================== */

:root {
  /* ── Meta ──────────────────────────────────────────────────────────────
     Hell-/Dunkel-Schema. Beeinflusst u. a. native Formular-Elemente.
     Werte: dark | light */
  --vc-color-scheme: dark;

  /* Schriftfamilie der gesamten Oberfläche.
     Eigene Webfont? Oben in der Datei per @import laden, z. B.:
       @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap');
     und hier referenzieren: 'Inter', system-ui, sans-serif */
  --vc-font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;

  /* ── Flächen ───────────────────────────────────────────────────────────
     Grundfarben der UI-Container (Sidebar, Dialoge, Karten). */
  --vc-bg: #0a0a0a;             /* App-Hintergrund / Canvas-Hintergrund beim Laden */
  --vc-surface: #181818;       /* Sidebar, Dialog-Flächen */
  --vc-surface-raised: #2a2a2a;/* erhöhte Elemente (Tracks, Chips, Inputs) */
  --vc-surface-hover: #3a3a3a; /* Hover-Fläche */
  --vc-border: #2a2a2a;        /* Standard-Rahmenfarbe */
  --vc-border-strong: #444444; /* betonte Rahmen */

  /* ── Text ──────────────────────────────────────────────────────────────*/
  --vc-text: #eeeeee;          /* Standard-Textfarbe */
  --vc-text-muted: #888888;    /* sekundärer Text */
  --vc-text-subtle: #666666;   /* dezente Hinweise */
  --vc-text-on-accent: #ffffff;/* Text auf Akzent-/Primärflächen */

  /* ── Akzent (Marke) ────────────────────────────────────────────────────
     Wird u. a. für aktive Zustände, Timeline/Abspielkopf und Slider-Punkte
     verwendet. `--vc-accent-rgb` MUSS dieselbe Farbe als "R, G, B" enthalten
     (ohne rgb()-Klammern) – wird für halbtransparente Hervorhebungen genutzt. */
  --vc-accent: #df3b00;
  --vc-accent-strong: #ff5722; /* reserviert — wird derzeit von keinem UI-Element gelesen */
  --vc-accent-rgb: 223, 59, 0; /* = --vc-accent als R, G, B */
  --vc-accent-muted: #3a3a3a;  /* gedämpfter Akzent-Hintergrund */

  /* ── Header (Kopfzeile mit Produktauswahl) ─────────────────────────────
     Nur relevant, wenn der Header sichtbar ist (sonst per ?header=0 ausblenden). */
  --vc-header-bg: transparent;     /* Hintergrund der Kopfzeile */
  --vc-header-text: #eeeeee;       /* Header-Text (aktives Produkt-Label, Menü-Icon) */
  --vc-header-text-muted: #888888; /* sekundärer Header-Text (inaktive Produkt-Labels) */
  --vc-header-border: #1a1a1a;     /* untere Trennlinie */
  --vc-header-border-width: 1px;   /* Stärke der Trennlinie (0px = keine) */
  --vc-header-padding: 12px 20px;  /* Innenabstand der Kopfzeile */

  /* ── Buttons ───────────────────────────────────────────────────────────*/
  --vc-button-bg: #2a2a2a;       /* Button-Fläche */
  --vc-button-bg-hover: #3a3a3a; /* Button-Fläche bei Hover */
  --vc-button-text: inherit;     /* Button-Textfarbe */
  --vc-button-border: #444444;   /* Button-Rahmen */
  --vc-button-radius: 4px;       /* Eckenradius der Buttons */
  /* Inaktiver Auswahl-Button (Platzhalter-/Szenen-Selector). Bewusst NICHT die
     Primärbutton-/Akzentfarbe — neutraler Ton, damit aktiv/inaktiv sichtbar trennt. */
  --vc-button-inactive-bg: #3a3a3a;   /* Fläche des nicht-aktiven Selector-Buttons */
  --vc-button-inactive-text: #eeeeee; /* dessen Textfarbe (auf der Fläche lesbar) */

  /* ── Slider / Zeitleiste ───────────────────────────────────────────────
     Die In/Out-Marken nutzen --vc-accent (s. o.); der Abspielkopf lässt sich
     darüber hinaus separat einfärben. */
  --vc-slider-track: #3a3a3a;    /* Track-Farbe der Regler */
  /* Abspielkopf (Linie + Griff) BEIDER Zeitleisten — Haupttimeline und
     Platzhalter-Mini-Leiste. Ohne diese Zeile folgt er --vc-accent. */
  --vc-playhead-color: #df3b00;
  /* Bereich außerhalb von In/Out in beiden Zeitleisten. Bei einem HELLEN Theme
     unbedingt beide Werte anpassen (s. Beispielblock unten) — sonst wirkt der
     inaktive Bereich dunkler als der aktive und die Timecodes verschwinden. */
  --vc-timeline-dim: rgba(0, 0, 0, 0.55);      /* Schleier außerhalb In/Out */
  --vc-timeline-dim-text: #ffffff;             /* Timecodes im Schleier */

  /* ── Vorschau-Canvas (Rahmen um das Video) ─────────────────────────────*/
  --vc-canvas-outline-color: #2a2a2a; /* Outline-Farbe des Canvas */
  --vc-canvas-outline-width: 1px;     /* Outline-Stärke (0px = kein Rahmen) */
  --vc-canvas-radius: 0px;            /* Abgerundete Canvas-Ecken (0px = eckig) */

  /* ── Lade-Spinner (während Hintergrund/Produkt lädt) ───────────────────
     Ohne diese Zeilen folgt der Spinner --vc-accent (Bogen) / --vc-border (Ring). */
  --vc-spinner-color: #df3b00; /* rotierender Bogen (Default: Akzent) */
  --vc-spinner-track: #2a2a2a; /* Ring/Spur (Default: Border) */
  --vc-spinner-size: 32px;     /* Durchmesser */

  /* ── Viewport-Rahmen (optionaler Rahmen um die ganze Fläche) ───────────*/
  --vc-frame-color: transparent; /* Farbe des Außenrahmens */
  --vc-frame-width: 0px;          /* Stärke (0px = aus) */

  /* ── Platzhalter (Plakatflächen ohne hochgeladenes Motiv) ──────────────
     Die generierte „A/B"-Platzhaltertextur. */
  --vc-placeholder-bg: #ffffff;     /* Hintergrund der Platzhalterfläche */
  --vc-placeholder-text: #000000;   /* Buchstabe/Beschriftung */
  --vc-placeholder-border: #000000; /* Rahmen der Platzhalterfläche */

  /* ── Dialoge & Eingabefelder ───────────────────────────────────────────
     Export-Dialog („Export als MP4") sowie alle Input-/Select-Felder
     (Dateiname, Auflösung, Effekt-Dropdown). Ohne diese Zeilen folgt alles
     den Surface-/Text-Defaults oben. */
  --vc-dialog-bg: #181818;                /* Dialog-Fläche */
  --vc-dialog-border: #2a2a2a;            /* Dialog-Rahmen */
  --vc-dialog-radius: 8px;                /* Eckenradius des Dialogs */
  --vc-dialog-backdrop: rgba(0,0,0,0.78); /* Abdunklung hinter dem Dialog */
  --vc-input-bg: #0a0a0a;                 /* Fläche von Input/Select */
  --vc-input-border: #2a2a2a;             /* Rahmen von Input/Select */
  --vc-input-text: #eeeeee;               /* Textfarbe in Input/Select */
  --vc-input-radius: 4px;                 /* Eckenradius von Input/Select */
  --vc-label-text: #888888;               /* Beschriftung über den Feldern */
}

/* =============================================================================
   BEISPIEL: helles Marken-Theme (zum Vergleich auskommentiert).
   Zum Ausprobieren das obige :root entfernen und dieses aktivieren.
   ============================================================================*/
/*
:root {
  --vc-color-scheme: light;
  --vc-bg: #ffffff;
  --vc-surface: #ffffff;
  --vc-surface-raised: #f2f2f2;
  --vc-surface-hover: #e8e8e8;
  --vc-border: #e2e2e2;
  --vc-border-strong: #c2c2c2;
  --vc-text: #1a1a22;
  --vc-text-muted: #5e5e68;
  --vc-text-subtle: #8c8c94;
  --vc-text-on-accent: #ffffff;
  --vc-accent: #0066ff;
  --vc-accent-strong: #0047b3;
  --vc-accent-rgb: 0, 102, 255;
  --vc-accent-muted: #d6e4ff;
  --vc-header-bg: #ffffff;
  --vc-header-text: #1a1a22;
  --vc-header-text-muted: #6a6a74;
  --vc-header-border: #e2e2e2;
  --vc-header-border-width: 1px;
  --vc-button-bg: #0066ff;
  --vc-button-bg-hover: #0047b3;
  --vc-button-text: #ffffff;
  --vc-button-border: #0066ff;
  --vc-button-radius: 6px;
  --vc-button-inactive-bg: #e8e8e8;
  --vc-button-inactive-text: #1a1a22;
  --vc-slider-track: #e0e0e0;
  --vc-timeline-dim: rgba(0, 0, 0, 0.10);
  --vc-timeline-dim-text: var(--vc-text-muted);
  --vc-canvas-outline-color: #0066ff;
  --vc-canvas-outline-width: 2px;
}
*/
```

### All CSS variables at a glance

You don't have to set every variable – unset variables keep the simulator/tenant default. Colors accept any valid CSS color (`#hex`, `rgb()`, `hsl()`, named).

| Variable | Effect | Default |
| --- | --- | --- |
| `--vc-color-scheme` | light/dark scheme (`dark · light`) – affects native form elements among others | `dark` |
| `--vc-font-family` | font family of the entire UI (load custom webfonts via `@import` at the top of the theme CSS) | `system-ui, …` |
| `--vc-bg` | app background / canvas background while loading | `#0a0a0a` |
| `--vc-surface` | sidebar, dialog surfaces | `#181818` |
| `--vc-surface-raised` | raised elements (tracks, chips, inputs) | `#2a2a2a` |
| `--vc-surface-hover` | hover surface | `#3a3a3a` |
| `--vc-border` | default border color | `#2a2a2a` |
| `--vc-border-strong` | emphasized borders | `#444444` |
| `--vc-text` | default text color | `#eeeeee` |
| `--vc-text-muted` | secondary text | `#888888` |
| `--vc-text-subtle` | subtle hints | `#666666` |
| `--vc-text-on-accent` | text on accent/primary surfaces | `#ffffff` |
| `--vc-accent` | brand accent: active states, in/out marks, slider handles | tenant theme |
| `--vc-accent-rgb` | the same color as “R, G, B” (without `rgb()` parentheses) – used for translucent highlights; **must** match `--vc-accent` | tenant theme |
| `--vc-accent-strong` | reserved – currently read by no UI element | – |
| `--vc-accent-muted` | muted accent background | `#3a3a3a` |
| `--vc-header-bg` | header background | `transparent` |
| `--vc-header-text` | header text (active product label, menu icon) | `#eeeeee` |
| `--vc-header-text-muted` | secondary header text (inactive product labels) | `#888888` |
| `--vc-header-border` | bottom divider of the header | `#1a1a1a` |
| `--vc-header-border-width` | divider width (`0px` = none) | `1px` |
| `--vc-header-padding` | header padding | `12px 20px` |
| `--vc-button-bg` | button surface | `#2a2a2a` |
| `--vc-button-bg-hover` | button surface on hover | `#3a3a3a` |
| `--vc-button-text` | button text color | `inherit` |
| `--vc-button-border` | button border | `#444444` |
| `--vc-button-radius` | button corner radius | `4px` |
| `--vc-button-inactive-bg` | surface of the **inactive** selector button (placeholder/scene selector) – deliberately neutral instead of accent, so active/inactive separate visibly | `#3a3a3a` |
| `--vc-button-inactive-text` | text color of the inactive selector button | `#eeeeee` |
| `--vc-slider-track` | track color of the sliders | `#3a3a3a` |
| `--vc-playhead-color` | playhead (line + handle) of **both** timelines – main timeline + placeholder mini bar | follows `--vc-accent` |
| `--vc-timeline-dim` | veil outside the in/out marks – adjust for light themes (see note) | `rgba(0, 0, 0, 0.55)` |
| `--vc-timeline-dim-text` | timecodes inside the veil – adjust for light themes (see note) | `#ffffff` |
| `--vc-canvas-outline-color` | outline color of the preview canvas | `#2a2a2a` |
| `--vc-canvas-outline-width` | outline width (`0px` = no frame) | `1px` |
| `--vc-canvas-radius` | rounded canvas corners (video, loading area and frame alike) | `0px` (square) |
| `--vc-spinner-color` | loading spinner: rotating arc | follows `--vc-accent` |
| `--vc-spinner-track` | loading spinner: ring/track | follows `--vc-border` |
| `--vc-spinner-size` | loading spinner: diameter | `32px` |
| `--vc-frame-color` | optional outer frame around the whole area: color | `transparent` |
| `--vc-frame-width` | optional outer frame: width (`0px` = off) | `0px` |
| `--vc-placeholder-bg` | background of the placeholder box (generated “A/B” texture) | `#ffffff` |
| `--vc-placeholder-text` | letter/label of the placeholder box | `#000000` |
| `--vc-placeholder-border` | border of the placeholder box | `#000000` |
| `--vc-dialog-bg` | dialog surface (“Export as MP4” dialog) | follows surface |
| `--vc-dialog-border` | dialog border | follows border |
| `--vc-dialog-radius` | dialog corner radius | `8px` |
| `--vc-dialog-backdrop` | dimming behind the dialog | `rgba(0,0,0,0.78)` |
| `--vc-input-bg` | input/select surface (incl. the effect dropdown on photo products) | follows surface |
| `--vc-input-border` | input/select border | follows border |
| `--vc-input-text` | text color in input/select | follows `--vc-text` |
| `--vc-input-radius` | input/select corner radius | `4px` |
| `--vc-label-text` | labels above the fields | follows `--vc-text-muted` |

- **No CORS needed:** the CSS file is loaded as a `<link>` (not via `fetch` like creatives) and may be cross-origin. CSS cannot run JavaScript.
- **URL-encode:** values for `?theme=`/`?bg=` must be URL-encoded (e.g. `%23` for `#`, `%2F` for `/`).
- **The loading spinner is themeable too:** `--vc-spinner-color` (rotating arc), `--vc-spinner-track` (ring) and `--vc-spinner-size` (diameter) – without an override it follows `--vc-accent` / `--vc-border` automatically.
- **Rounded canvas corners:** `--vc-canvas-radius` (default `0px`) rounds the preview canvas – video, loading area and frame alike – e.g. `--vc-canvas-radius: 16px;` in the `?theme=` CSS.
- **Color the playhead separately:** `--vc-playhead-color` colors the playhead of **both** timelines (main timeline + placeholder mini bar) independently of the accent; without an override it follows `--vc-accent`. Custom CSS rules on the `vc-main-playhead` class do **not** apply (the elements are styled inline) – use the variable.
- **⚠️ Timeline dimming – must be adjusted for light themes:** the area outside the in/out marks is dimmed via `--vc-timeline-dim` (veil) and `--vc-timeline-dim-text` (the timecodes inside it). The defaults target **dark** themes (`rgba(0,0,0,0.55)` + white). For a **light** theme override **both** – a proven pair: `--vc-timeline-dim: rgba(0, 0, 0, 0.10);` and `--vc-timeline-dim-text: var(--vc-text-muted);`.
- **Dialogs & input fields are themeable too:** the export dialog (`--vc-dialog-bg`, `--vc-dialog-border`, `--vc-dialog-radius`, `--vc-dialog-backdrop`) and all input/select fields incl. the photo-effect dropdown (`--vc-input-bg`, `--vc-input-border`, `--vc-input-text`, `--vc-input-radius`, `--vc-label-text`) can be styled individually – without an override they follow the surface/text variables.
- **Only placeholder colors?** You don't need a theme file for that: `?placeholderBg=`/`?placeholderText=`/`?placeholderBorder=` set the placeholder box directly via URL (see URL parameters above) and override the theme + `?theme=`.
- You'll find every overridable CSS variable in the table and the theme template below.

## Step 8 — Reference: postMessage API

Build your own UI and drive the iframe via `window.postMessage`. The iframe also sends events back (including the list of available product codes).

### Incoming commands (your page → iframe)

| Message | Effect |
| --- | --- |
| `{ type: "kreativsimulator:setProduct", code: "ec5472c2" }` | Loads the product with this code. |
| `{ type: "kreativsimulator:setCreative", url: "https://…/creative.png" }` | Places the image/video as the creative on **all** placeholders. |
| `{ type: "kreativsimulator:setCreative", url: "…", loader: "Loader A" }` | Fills **only** the named placeholder. |
| `{ type: "kreativsimulator:clearCreative", loader?: "Loader A" }` | Resets one (or without `loader` all) placeholders to the default. |
| `{ type: "kreativsimulator:setLanguage", lang: "fr" }` | Switches the UI language (de/en/fr/it). |
| `{ type: "kreativsimulator:setEffect", id: "static-5s" }` | **Photo products:** selects an entry from `ready.effects` – a static preset (`static-5s`/`static-10s`, always present) or a named Ken-Burns effect. `id: null` = static still (1 frame). Changes the timeline length → a `playbackState` follows. |
| `{ type: "kreativsimulator:getEffects", code: "ec5472c2" }` | Queries the effect list of **any** product **without** activating it (live view stays unchanged) → reply as an `effects` event. For a per-product effect picker before calling `setProduct`. |
| `{ type: "kreativsimulator:play" }` · `pause` · `stop` | Starts / pauses / stops (back to start) playback. |
| `{ type: "kreativsimulator:stepForward", frames?: 1 }` · `stepBackward` | Steps `frames` frames forward/back (default 1) and pauses. |
| `{ type: "kreativsimulator:seek", frame: 120 }` | Jumps to an absolute frame index (`0…frameCount-1`). |
| `{ type: "kreativsimulator:export", filename?, width?, height?, watermark? }` | Starts the MP4 export. With `width`+`height` directly at **any** resolution; without, the export dialog opens. `watermark: true` adds the “Vorschau” (preview) watermark (see notes). |
| `{ type: "kreativsimulator:setChrome", header?, sidebar?, playback?, timeline?, export?, effects? }` | Toggles individual UI areas at runtime (each `true`/`false`). `effects` = effect dropdown (photo products). |
| `{ type: "kreativsimulator:fullscreen", enabled? }` | Fullscreen: `true`=on, `false`=off, without `enabled`=toggle (see note). |
| `{ type: "kreativsimulator:getState" }` | Requests the current playback state → reply as `playbackState`. |

### Outgoing events (iframe → your page)

```js
{ source: "kreativsimulator", v: 1, type: "ready",
  product: "ec5472c2", locale: "de",
  products: [{ code: "ec5472c2", name: "City-Light …" }, …],
  loaders: [{ name: "Loader A", aspectRatio: 2.96 }, …],
  // native render resolution (16:9) + suggested export presets
  render: { width: 1280, height: 720, aspectRatio: 1.7778,
            presets: [{ label: "E-Mail / WhatsApp (640×360)", width: 640, height: 360 },
                      { label: "Standard HD (1280×720)", width: 1280, height: 720 },
                      { label: "YouTube / Full HD (1920×1080)", width: 1920, height: 1080 }] },
  // photo products (productType "bild"): effects[] = two always-present static
  // presets (static-5s, static-10s) + named Ken-Burns effects; default = static-5s.
  // video products report isPhoto:false, effects:[], selectedEffect:null.
  isPhoto: true,
  effects: [{ id: "static-5s", name: "Static (5s)", durationFrames: 125 },
            { id: "static-10s", name: "Static (10s)", durationFrames: 250 },
            { id: "fx-a", name: "Effect A", durationFrames: 125 }, …],
  selectedEffect: "static-5s" }

{ source: "kreativsimulator", v: 1, type: "productChanged", code: "ec5472c2",
  loaders: [{ name: "Loader A", aspectRatio: 2.96 }, …],
  isPhoto: true,
  effects: [{ id: "static-5s", name: "Static (5s)", durationFrames: 125 },
            { id: "static-10s", name: "Static (10s)", durationFrames: 250 },
            { id: "fx-a", name: "Effect A", durationFrames: 125 }, …],
  selectedEffect: "static-5s" }

// reply to getEffects {code}: a product's effect list WITHOUT activating it.
// same fields as productChanged (incl. the static presets), plus code.
// video products report isPhoto:false, effects:[], selectedEffect:null.
{ source: "kreativsimulator", v: 1, type: "effects", code: "ec5472c2",
  isPhoto: true,
  effects: [{ id: "static-5s", name: "Static (5s)", durationFrames: 125 },
            { id: "static-10s", name: "Static (10s)", durationFrames: 250 },
            { id: "fx-a", name: "Effect A", durationFrames: 125 }, …],
  selectedEffect: "static-5s" }

// playback state: on play/pause, ~4×/s while playing, after seek/stop/step, and on getState
// for photo products, frameCount equals the active effect's duration
{ source: "kreativsimulator", v: 1, type: "playbackState",
  frame: 120, playing: true, frameCount: 250, fps: 25 }

// confirmation after setChrome, with the new visibility state
{ source: "kreativsimulator", v: 1, type: "chromeChanged",
  header: false, sidebar: false, playback: true, timeline: true, export: false, effects: false }

// fullscreen state — on every change (e.g. ESC); for syncing your own button
{ source: "kreativsimulator", v: 1, type: "fullscreenChanged", fullscreen: true }

{ source: "kreativsimulator", v: 1, type: "error", message: "…", code, loader }
```

Always filter incoming messages on `source === "kreativsimulator"`. `v` is the protocol version (currently 1).

### Complete example

Build your own buttons from the product list, place a creative on a targeted placeholder (correct ordering), and a custom resolution picker from `render.presets`:

```html
<iframe id="ks" src="https://demo.kreativsimulator.com/"
        style="width:100%;height:720px;border:0" allow="fullscreen"></iframe>

<script>
  const iframe = document.getElementById("ks");
  const send = (msg) => iframe.contentWindow.postMessage(msg, "*");

  window.addEventListener("message", (e) => {
    const m = e.data;
    if (!m || m.source !== "kreativsimulator") return;

    if (m.type === "ready") {
      // Build your own buttons from the product list
      for (const p of m.products) {
        const btn = document.createElement("button");
        btn.textContent = p.name;
        btn.onclick = () =>
          send({ type: "kreativsimulator:setProduct", code: p.code });
        document.body.appendChild(btn);
      }
      // Build your own resolution picker + export button from m.render.presets
      const sel = document.createElement("select");
      for (const r of m.render.presets) {
        const o = document.createElement("option");
        o.value = r.width + "x" + r.height;
        o.textContent = r.label;
        sel.appendChild(o);
      }
      const exportBtn = document.createElement("button");
      exportBtn.textContent = "Export";
      exportBtn.onclick = () => {
        const [width, height] = sel.value.split("x").map(Number);
        send({ type: "kreativsimulator:export", filename: "creation", width, height });
      };
      document.body.append(sel, exportBtn);
    }

    // The product is active now → only here may you target a placeholder.
    if (m.type === "productChanged") {
      const first = m.loaders[0];                // take the name from the event
      if (first) {
        send({
          type: "kreativsimulator:setCreative",
          url: "https://cdn.your-agency.com/creative.png",
          loader: first.name,                    // this placeholder only
        });
      }
    }

    if (m.type === "error") console.warn("KS error:", m.message);
  });

  // Creative on ALL placeholders (no loader) — fine right after `ready`:
  // send({ type: "kreativsimulator:setCreative", url: "https://cdn.your-agency.com/creative.png" });
</script>
```

### Important

- **Ordering on product change:** after `setProduct` the new product is only active once the `productChanged` event arrives. Send placeholder-targeted commands (`setCreative`/`clearCreative` with `loader`) only afterwards – before that the iframe only knows the old product's placeholders. A product change also resets all creatives.
- **Your own controls:** playback (`play`/`pause`/`stop`/`step…`/`seek`) and `export` always work – whether or not the built-in buttons are visible. Hide them with `?embed=kiosk` and drive everything yourself.
- **Pick the export resolution:** `export` with `width`+`height` renders at any resolution. Native resolution (1280×720, 16:9) and presets come from `ready.render` – keep the 16:9 aspect ratio or the image distorts.
- **Add the watermark:** `export` with `watermark: true` composites the diagonally tiled “Vorschau” (preview) watermark into the exported MP4 – e.g. for non-binding previews. The flag is **additive only**: `false` (or omitted) gives the normal behavior; on the **demo tenant** the watermark is always on and cannot be disabled. To mark **all** exports of an embed (including those from the built-in export dialog), use the URL parameter `?watermark=1` instead.
- **Photo products & Ken-Burns effects:** some products are still images with named camera moves. `ready`/`productChanged` then report `isPhoto: true` and a list of `effects` (`{ id, name, durationFrames }`). The list **always** starts with two static presets (`static-5s` = 5 s, `static-10s` = 10 s), followed by the named effects; on load `static-5s` is selected automatically (`selectedEffect`). Use `setEffect { id }` to switch entry (or `id: null` for a pure still image, 1 frame). Playback and export work just like video; `playbackState.frameCount` equals the active entry's duration.
- **Pre-fetch effects (without switching product):** `ready`/`productChanged` only carry the effects of the **active** product. To build an effect picker for **other** products without switching the live view, send `getEffects { code }` and listen for the `effects` event (same fields as `productChanged`, plus `code`). That gives you each effect's `id`/`name` to drive later via `setEffect` (after `setProduct` to that code).
- **Turn fullscreen on reliably:** browsers only allow enabling it as a direct reaction to a user gesture – a `fullscreen` postMessage usually lacks that and may be blocked. So call `iframe.requestFullscreen()` **on your page** on click (the iframe needs `allow="fullscreen"`); the postMessage is fine for off/toggle. The state comes via `fullscreenChanged`.
- **Placeholder names** are not free-form – use exactly one `name` from the `loaders[]` array of `ready` or `productChanged`.
- **CORS:** on `setCreative` the iframe fetches the image/video URL. The host of your file must therefore send `Access-Control-Allow-Origin`, otherwise an `error` event comes back.
- **Security:** which pages may embed the iframe is set server-side via `frame-ancestors` – only those can send messages at all.

## Good to know

- No API key needed – authentication runs entirely server-side.
- Users can operate the simulator directly in the iframe **or** you drive it via postMessage from your page.
- The iframe loads video footage – a stable connection is recommended.

## Troubleshooting

### The iframe stays blank / white area

Most likely the embedding domain is not whitelisted. The browser console (F12) then shows “Refused to frame … frame-ancestors …”. Send us the exact domain (incl. https:// and any subdomain) and we'll whitelist it.

### The iframe shows “403”

The tenant is not yet fully configured server-side (no API key stored). Please contact us.

### A creative doesn't appear (error event)

The creative URL sends no CORS, or the loader name doesn't match the active product. Check the image URL's Access-Control-Allow-Origin and send loader-targeted commands only after the productChanged event.

## Ready to embed?

Send us your domain(s) – we'll whitelist the embed and you'll be live in minutes.

Contact: https://virtualcampaign.dev/en/#contact — we whitelist your domain(s) and you are live in minutes.
