JavaScript tag reference

Install the reusable OmniLab tag, configure its options, and handle the messages an embedded experience sends your page.

6 min read

Install the reusable tag so one page can load different experiences and the frame behaves correctly without you writing the sizing and panel logic yourself. This page is for your developer.

The one value you need first

The snippet below needs the address your experiences are served from. Everything else is fixed.

That address must be a subdomain of the site you are embedding on — see the requirement below — so for a site at www.yourbrand.com it will be something like experience.yourbrand.com. Set it up before you build. See Custom domain.

The experience must share your site's domain

If you embed an experience served from a different domain than the page — including the default your-brand.topage.co inside www.yourbrand.com — the browser treats the frame as third-party and blocks or isolates its cookies. Sign-in does not persist, and the login round-trip loses its state partway through.

Serving the experience from a subdomain of your own site makes the frame same-site, and all of that works normally. This is a requirement for embedding, not a preference.

Install the tag

The script address depends on which OmniLab environment your experiences run on. Use production unless you are deliberately testing against another one, and keep the script host and the experience domain on the same environment — mixing them produces a frame that loads and then behaves as though the campaign does not exist.

EnvironmentScript address
Productionhttps://cdn.21-digital.com/omplayer.js
UAThttps://cdn-uat.21-digital.com/omplayer.js
Developmenthttps://cdn-dev.21-digital.com/omplayer.js
Container + OmniLab tag
<div id="omnilab-container"></div>

<script>
  (function (b, o, n, u, s) {
    var a, t;
    a = b.createElement(u);
    a.async = 1;
    a.src = s;
    t = b.getElementsByTagName(u)[0];
    t.parentNode.insertBefore(a, t);
    o[n] = o[n] || [];
  })(document, window, "_om_async", "script", "https://cdn.21-digital.com/omplayer.js");

  _om_async.push([
    "init",
    {
      domain: "experience.example.com",
      targetId: "omnilab-container",
      queryParam: "id",
    },
  ]);
</script>

With this in place, the host page's own address decides which experience loads:

  • https://www.example.com/promo?id=summer-campaign
  • https://www.example.com/promo?id=summer-campaign?c=<touchpoint-id>

The value of id is the path after the OmniLab domain. A nested ? survives as-is, because the tag reads everything up to the next &. If you need to pass more than one nested parameter, URL-encode the whole value so the & does not truncate it.

You do not need to add embedded=1 here — the tag appends it, along with the host page's address, when it builds the frame. That parameter is only yours to add when you write the iframe yourself.

What the tag needs from your page

Three things, none of them obvious from the snippet:

Leave the container empty. The tag owns everything inside #omnilab-container — the loading state, the frame, its styling, and its width and height. If your framework renders a placeholder or a spinner inside that element, the two will fight over the same nodes. Render the empty div and let the tag fill it.

Allow both hosts in your Content Security Policy. If your site sends a CSP — most do — the embed is blocked silently until you add:

script-src  … https://cdn.21-digital.com
frame-src   … https://experience.yourbrand.com

Allowlist the script host for the environment you are actually using. If you test against UAT from the same site, that host needs allowlisting too, or the test fails in a way that looks like a broken campaign.

frame-src names the domain your experiences are served from — your own subdomain, per the requirement above. This is the most common reason an embed that worked in a test page fails on the real site: the test page had no policy and the real one does.

Re-initialise after a client-side navigation. The script guards against running twice, so in a single-page application that navigates away and back, the tag will not restart itself. Call init again once the script is loaded.

Options

Pass these in the same object as domain and targetId.

OptionDefaultWhat it does
domainThe address your experiences are served from — your OmniLab subdomain or your own. Required.
targetIdThe id of the container the frame is inserted into. Required.
queryParam"id"Which host-page query parameter carries the OmniLab path
width"100%"Width applied to the frame and its container
height"100px"Starting height, before the experience reports its own
iframeId"omnilab-iframe"id given to the generated frame
iframeClass""Extra CSS classes on the frame
iframeStyle"border: none;"Inline style on the frame
allowFullscreenoffAdds the fullscreen attributes
sandboxAttributesunsetSets a sandbox attribute with the value you give
stickyHeaderSelectorauto-detectedA selector for your sticky header, so panels open below it
topOffset0Fixed pixels to keep clear at the top, instead of header detection
scrollIntoViewOnNavigatetrueScrolls the embed back into view when the visitor navigates inside it
instantGameHeightunsetForces a fixed frame height for viewport-sized pages
minFrameHeight320Floor applied to the measured viewport height
maxFrameHeightunsetCeiling applied to the measured viewport height

Check these against your own CSS

These options style the frame the tag generates. If you also style #omnilab-iframe from your own stylesheet, make sure the two agree — whichever loses the specificity contest silently wins the layout.

Sticky headers

The tag finds a standard page header on its own and keeps panels clear of it, including when the header scrolls away. Name yours explicitly when it is not picked up:

Point the tag at your own header
_om_async.push([
  "init",
  {
    domain: "experience.example.com",
    targetId: "omnilab-container",
    stickyHeaderSelector: "#site-nav",
  },
]);

Use a stable selector — an id or a data attribute, not a hashed CSS-module class that changes between builds. Pass false to opt out of header detection and use topOffset instead.

Writing the parent page by hand

Only do this if something prevents you loading the tag. The tag exists precisely so you do not have to implement what follows, and it is the path OmniLab tests against.

Writing the parent yourself means you own the frame's height, the placement of any panel the experience opens, and keeping both correct as the experience changes. Everything below is what the tag would otherwise do for you.

You still need the CSP entries

The two policy directives above apply either way — the frame host has to be allowed in frame-src whether the tag creates the frame or you do.

Sizing

The experience announces how it wants to be sized on every page load, and again whenever it changes:

Message from OmniLabWhat it means
{ type: "sizing", mode: "content" }The page reports its own height. Size the frame from the resize messages that follow.
{ type: "sizing", mode: "viewport" }The page fills whatever frame it is given. Size the frame yourself, from your own viewport.

Instant games are viewport-sized, because they measure window.innerHeight — which, inside a frame, is the frame's height. Sizing the frame from a height derived that way closes a loop with no outside input and the frame freezes. OmniLab suppresses resize entirely in viewport mode for that reason.

The mode belongs to the page, not the embed. Navigating from a game back to a landing page returns to content sizing and the page re-announces it, so hold the mode in a variable rather than deciding once at load.

Parent page resize listener
<script>
  window.addEventListener("message", (event) => {
    if (event.origin !== "https://experience.example.com") return;

    const data = event.data;
    if (data?.type !== "resize" || data?.target !== "body") return;
    if (typeof data?.data?.height !== "number" || data.data.height < 100) return;

    const iframe = document.querySelector("#omnilab-embed");
    if (iframe) iframe.style.height = data.data.height + "px";
  });
</script>

OmniLab coalesces these to one message per animation frame and drops repeats of the same height, so apply each one directly without debouncing. Ignore anything below 100 pixels (a page mid-layout) and anything arriving while a panel is open (the frame's viewport is then the panel box; applying it shrinks the embed once the panel closes).

Panels

From OmniLabWhen it fires
open-modalThe first panel opens. Nested panels do not repeat it.
close-modal (with source)The last panel closes, so it always means fully closed.
To OmniLabWhat it does
ready-to-open-modal (with parentHeight, parentWidth, scrollTop, stickyOffset, position)Acknowledges open-modal and hands the panel the slice of the frame the visitor can actually see. Send it on every open-modal, nested ones included.
ready-to-close-modal (with source)Closes the panel on top of the stack. Send it when your own UI dismisses it.

If you never send ready-to-open-modal, panels still open — they fall back to a default full-height presentation instead of being placed in the visible slice.

Check event.origin against your OmniLab domain in anything you ship. The tag does this already; a hand-written listener that skips it will act on messages from any framed page.

On this page