Skip to content
Back to home

Embed the Kreativsimulator via <iframe>

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.

For AI coding agents (Claude Code, Cursor & co.)

This entire guide is also available as clean Markdown – one URL your coding agent can read directly. Just give it this prompt:

Prompt
Read https://virtualcampaign.dev/embed.md and embed the Kreativsimulator into our website via iframe.

/embed.md /llms.txt

In addition, https://virtualcampaign.dev/llms.txt links the docs following the llms.txt convention – AI agents discover them there automatically.

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)

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.

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

Embed URL: https://<dein-mandant>.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 &):

ParameterValuesEffectDefault
langde · en · fr · itUI languageTenant → browser → de
productProduct code (ec5472c2)Loads this product directly (deep link). Unknown code → first product.First product
header0 · 1Show/hide the header (product selector)1 (visible)
sidebar0 · 1Show/hide the sidebar (incl. mobile drawer + hamburger)1 (visible)
playback0 · 1Show/hide the playback/transport buttons in the bar1 (visible)
timeline0 · 1Show/hide the timeline (scrubbing bar)1 (visible)
export0 · 1Show/hide the “Export creation” button1 (visible)
effects0 · 1Show/hide the effect dropdown (photo products only) – e.g. for your own picker1 (visible)
fullscreen0 · 1Fullscreen icon: 0=never, 1=always (incl. desktop); omitted = mobile / in fullscreen onlyadaptive
embedminimal · kioskShorthand: hides header, sidebar and export (playback bar stays)
themeURL to a CSS fileLoads a custom theme (CSS variables) that overrides the tenant theme
bgany CSS color, transparent or image URLBackground of the area around the canvas (letterbox/frame), not the videoTheme background
stage<width>x<height> (1280x720)Fixed display pixel size of the canvas instead of fluid widthfluid (100%)
placeholderBgany CSS colorBackground of the placeholder box (loader without a creative) – direct shortcut, no ?theme= fileTheme/default (#ffffff)
placeholderTextany CSS colorColor of the letter inside the placeholder boxTheme/default (#000000)
placeholderBorderany CSS colorBorder color of the placeholder boxTheme/default (#000000)
watermark0 · 11 = 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

View code URL
URL
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:

View code HTML
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:

View code HTML
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:

View code HTML + JS
HTML + JS
<!-- 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

View code HTML
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

Download template ks-theme.css
View template kreativsimulator-theme-template.css
kreativsimulator-theme-template.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).

VariableEffectDefault
--vc-color-schemelight/dark scheme (dark · light) – affects native form elements among othersdark
--vc-font-familyfont family of the entire UI (load custom webfonts via @import at the top of the theme CSS)system-ui, …
--vc-bgapp background / canvas background while loading#0a0a0a
--vc-surfacesidebar, dialog surfaces#181818
--vc-surface-raisedraised elements (tracks, chips, inputs)#2a2a2a
--vc-surface-hoverhover surface#3a3a3a
--vc-borderdefault border color#2a2a2a
--vc-border-strongemphasized borders#444444
--vc-textdefault text color#eeeeee
--vc-text-mutedsecondary text#888888
--vc-text-subtlesubtle hints#666666
--vc-text-on-accenttext on accent/primary surfaces#ffffff
--vc-accentbrand accent: active states, in/out marks, slider handlestenant theme
--vc-accent-rgbthe same color as “R, G, B” (without rgb() parentheses) – used for translucent highlights; must match --vc-accenttenant theme
--vc-accent-strongreserved – currently read by no UI element
--vc-accent-mutedmuted accent background#3a3a3a
--vc-header-bgheader backgroundtransparent
--vc-header-textheader text (active product label, menu icon)#eeeeee
--vc-header-text-mutedsecondary header text (inactive product labels)#888888
--vc-header-borderbottom divider of the header#1a1a1a
--vc-header-border-widthdivider width (0px = none)1px
--vc-header-paddingheader padding12px 20px
--vc-button-bgbutton surface#2a2a2a
--vc-button-bg-hoverbutton surface on hover#3a3a3a
--vc-button-textbutton text colorinherit
--vc-button-borderbutton border#444444
--vc-button-radiusbutton corner radius4px
--vc-button-inactive-bgsurface of the inactive selector button (placeholder/scene selector) – deliberately neutral instead of accent, so active/inactive separate visibly#3a3a3a
--vc-button-inactive-texttext color of the inactive selector button#eeeeee
--vc-slider-tracktrack color of the sliders#3a3a3a
--vc-playhead-colorplayhead (line + handle) of both timelines – main timeline + placeholder mini barfollows --vc-accent
--vc-timeline-dimveil outside the in/out marks – adjust for light themes (see note)rgba(0, 0, 0, 0.55)
--vc-timeline-dim-texttimecodes inside the veil – adjust for light themes (see note)#ffffff
--vc-canvas-outline-coloroutline color of the preview canvas#2a2a2a
--vc-canvas-outline-widthoutline width (0px = no frame)1px
--vc-canvas-radiusrounded canvas corners (video, loading area and frame alike)0px (square)
--vc-spinner-colorloading spinner: rotating arcfollows --vc-accent
--vc-spinner-trackloading spinner: ring/trackfollows --vc-border
--vc-spinner-sizeloading spinner: diameter32px
--vc-frame-coloroptional outer frame around the whole area: colortransparent
--vc-frame-widthoptional outer frame: width (0px = off)0px
--vc-placeholder-bgbackground of the placeholder box (generated “A/B” texture)#ffffff
--vc-placeholder-textletter/label of the placeholder box#000000
--vc-placeholder-borderborder of the placeholder box#000000
--vc-dialog-bgdialog surface (“Export as MP4” dialog)follows surface
--vc-dialog-borderdialog borderfollows border
--vc-dialog-radiusdialog corner radius8px
--vc-dialog-backdropdimming behind the dialogrgba(0,0,0,0.78)
--vc-input-bginput/select surface (incl. the effect dropdown on photo products)follows surface
--vc-input-borderinput/select borderfollows border
--vc-input-texttext color in input/selectfollows --vc-text
--vc-input-radiusinput/select corner radius4px
--vc-label-textlabels above the fieldsfollows --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 7 · Interactive

Try remote control live

Here you see the real iframe (left) next to a completely custom control UI (right) – your own product images, your own creatives, your own buttons. Every click sends a postMessage to the iframe. This is exactly how you rebuild it on your site; the matching code is right below.

Real iframe from demo.kreativsimulator.com Open in new tab

Show / hide chrome

Hide header/sidebar/buttons live → setChrome. “Canvas only” hides everything.

Your playback bar

Works even without visible buttons. Scrubber via seek, synced via playbackState.

Your product picker

Your page builds these cards from the ready event. Click → setProduct. Photo products show their effect count via getEffects (pre-fetched, no product switch).

Your creatives

Your own placeholder creatives. Click → setCreative (loads the image via CORS).

Your own file per placeholder

Drag an image onto A or B → setCreative with loader. In production you upload the file to your CORS host and pass the URL; here it's a data URL for the demo.

Export

Resolution from ready.render.presets. Click → export (MP4).

Load variants (URL parameters)

Reloads the iframe with parameters – this is how embed/bg/stage take effect.

Placeholder colors (URL)

Brands just the placeholder box directly via URL – no ?theme= file. “Apply” reloads the iframe with ?placeholderBg/Text/Border.

Language

Event log (iframe → your page)

  • No events received yet …

The following code drives exactly this live demo. Use it as your starting point:

View code HTML + JS
HTML + JS
<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>
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)

MessageEffect
{ 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 · stopStarts / pauses / stops (back to start) playback.
{ type: "kreativsimulator:stepForward", frames?: 1 } · stepBackwardSteps 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)

View code JS
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:

View code HTML + JS
HTML + JS
<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.