Developer documentation

Ignis Popups for theme developers.Contract v1

Last updated 7 September 2026. Applies to the Ignis app for Shopify.

Three HTML attributes and one JavaScript global. Put an Ignis popup anywhere in your theme, with your own markup and your own styling, and keep every popup a merchant builds in Ignis working without a code change.

Most merchants never read this page. Ignis ships app blocks they drag into the theme editor, and those blocks emit exactly the attributes documented below. This page is for the case where a block is not enough: you want the trigger to be your own button, the signup form to sit inside your own layout, or the back in stock control to live in a sold-out state your theme already renders.

The blocks and this contract are the same code path. An Ignis app block's only job is to emit one of these attributes. Nothing here is a lesser or parallel integration, and nothing here can fall behind the blocks.

  1. Before anything works: the app embed
  2. How a mount finds its popup
  3. Surface 1. Inline signup form
  4. Surface 2. Trigger
  5. Surface 3. Back in stock
  6. Rules the runtime enforces
  7. Migrating off an existing popup engine
  8. Emailing a late code from Klaviyo

Before anything works: the app embed

Every surface below is rendered by one script, loaded by the Ignis Popup Lab app embed. Turn it on in Theme settings, App embeds. App embeds are off by default, so installing the app alone changes nothing on any storefront.

Without the embed, the attributes below are inert markup. They render nothing and throw nothing. In the theme editor, Ignis leaves a visible note in an unbound mount saying which embed to enable.

How a mount finds its popup

Each attribute names a placement, not an experiment. A popup built in Ignis is assigned a placement, and the running experiment for that placement renders into any matching mount on the page.

You never need an ID. An attribute with an empty value binds to whichever experiment is currently running for its placement. This is the documented default, and it is why a merchant can swap which popup appears in their footer entirely inside Ignis, with no theme edit and no redeploy.

<!-- binds to the running signup form experiment -->
<div data-ignis-popup-embed></div>

<!-- binds to one named experiment: only for a store running several -->
<div data-ignis-popup-embed="summer-footer-v2"></div>

The experiment key is the one you see in the Ignis dashboard. Use it only when two experiments run on the same surface at once, which is rare. If two do, and no mount names one, the older experiment wins, on every page load and for every visitor.

Surface 1. Inline signup form footer

An email capture rendered inline, in your layout. Give Ignis an empty element and it fills it.

<div data-ignis-popup-embed></div>

Ignis replaces the element's contents with a form. The visitor enters an email, and the full popup sheet takes over from there: it subscribes them, runs the phone step if the experiment has one, and reveals the code. The inline form and the popup are one flow, so the funnel reports for the experiment count them together.

What Ignis renders inside it

ClassElement
.igp-embedthe form
.igp-embed__inputemail field
.igp-embed__btnsubmit button
.igp-embed__errvalidation message, hidden until needed
.igp-embed__recallreplaces the form once this visitor has already won a code

These elements inherit font-family, color and border-radius from their surroundings and draw their borders in currentColor, so an unstyled mount already looks like your theme. Restyle any of them freely. Ignis sets no colours of its own here.

One inline form per page. If several mounts match, only the first in document order renders and the rest get data-ignis-popup-suppressed. Two identical signup forms on one page is a bug, and it would count every view twice.

Surface 2. Trigger header

Any element that opens the popup on click. Your markup, your styling, entirely.

<button type="button" class="my-announcement-cta" data-ignis-popup-open>
  Get 10% off
</button>

Many triggers are supported and correct. An announcement bar and a nav link opening the same offer is a normal store, and Ignis binds every matching element.

Ignis adds a click handler and a keydown handler for Enter and Space, so a non-button element still works for keyboard users. It sets no styles and adds no classes. If the visitor has already won a code through this experiment, the trigger reopens their code instead of asking again.

Sharing an element with another popup tool

A trigger is usually an element your theme already owns, and it often carries a hook from whatever you used before. Most of those tools listen on document rather than on the element, so their handler still runs after ours and the shopper gets two popups.

Ignis never removes another vendor's attribute. Instead, once it has bound a trigger it sets data-ignis-popup-bound="1" on that element. Check for it in your own fallback:

document.addEventListener('click', function (e) {
  var el = e.target.closest('[data-my-old-popup]');
  if (!el) return;
  if (el.hasAttribute('data-ignis-popup-bound')) return;  // Ignis owns this click
  openMyOldPopup(el);
});

The attribute appears only when an experiment actually bound the element, so your fallback keeps working while the Ignis experiment is paused, absent, or still being drafted. That is what makes it safe to leave both hooks in place permanently instead of cutting over in one step.

Surface 3. Back in stock bis

Two ways in. Use the block if you want Ignis to render and manage the button; use the global if your theme already has a sold-out state and you only want our sheet.

The global

window.ignis.popup.openBackInStock(variantId, product)
ArgumentTypeNotes
variantIdstring or numberRequired. The Shopify variant id, not the product id.
product.product_namestringOptional. Shown in the sheet's copy.
product.size_labelstringOptional. The variant title, if you want it named.

🔴 It returns true only once the sheet is actually on screen. A false means nothing was rendered: no running back in stock experiment, the app embed is off, the experiment is misconfigured, or another sheet is already open.

Always run your own fallback on false. Treating the call as if it always succeeds is how a shopper ends up with no way to be notified at all, on a page that looks like it worked.

// The pattern. Ours if it opens, yours if it does not.
function onNotifyClick(variantId) {
  var opened = window.ignis
    && window.ignis.popup
    && window.ignis.popup.openBackInStock(variantId, {
         product_name: {{ product.title | json }},
         size_label: currentVariantTitle()
       });
  if (!opened) myOwnNotifyForm(variantId);
}

The global is defined only once a back in stock experiment is running, so guard the call as above. Ignis subscribes the shopper to that exact variant through Klaviyo, then offers an optional text alert if the experiment has SMS configured.

The block

The Ignis back in stock app block renders a button that appears only while the selected variant is sold out. It reads sold-out variants from Liquid at render time, and resolves the selected variant from the product form's [name="id"] input and the ?variant= URL parameter. It scrapes no theme markup, so a theme update cannot break it.

Only sold-out variants are rendered into the page, so a product with two hundred variants and two sold out carries two entries rather than two hundred.

Stock is re-checked in the browser. The Liquid snapshot is only as fresh as your cached product page, and Shopify caches those hard. A variant that sold out an hour ago can still render as available, which would hide the button at exactly the moment demand is highest.

The block re-reads /products/{handle}.js on load and prefers that answer. It is a separate cache entry and in practice fresher, though not a guarantee. A re-check that fails is ignored, so it can only ever add a chance to catch the change, never remove a button that is already correct.

Rules the runtime enforces

RuleWhy
One inline form per page Two identical signup forms is a bug, and it double-counts views. First in document order wins.
Many triggers per page An announcement bar and a nav link opening the same offer is normal.
One experiment per placement Two rendering at once would double-fire the funnel and each would corrupt the other's results. The older one wins, deterministically.
One sheet at a time A second open call returns false rather than stacking two dialogs.
Nothing renders without a running experiment A mount with no experiment behind it stays empty. It is never an error and never a placeholder.

Theme editor

Ignis re-runs its mount pass on shopify:section:load, :reorder, :unload, :block:select and :block:deselect, so a block added live binds without a reload. Mounting is idempotent: a re-scan never renders a second form or re-fires an event. On the storefront there is no observer at all, so this costs real visitors nothing.

Migrating off an existing popup engine

If your theme already runs its own popup engine, Ignis stands down completely by default. Two engines rendering the same popup would double-fire every event and corrupt your test results, so the safe assumption is that yours owns everything.

That default makes a cutover all or nothing, which is rarely what you want. Declare what your engine owns and Ignis serves the rest:

// In your engine, before Ignis loads.
window.__popupLabPlacements = ['popup', 'header', 'bis'];

Ignis then renders the footer placement and leaves the other three to you. Remove one entry at a time, watch the funnel, and move the next. Delete the array and your engine when the list is empty.

The value must be an array. Anything else is treated as owning every placement, so a typo fails safe rather than double-rendering.

window.__ignisPopupStandDown = true silences Ignis popups entirely, regardless of any declaration.

Emailing a late code from Klaviyo merchants

Ignis never emails your popup shoppers itself. When a popup code has to be sent after the fact, Ignis hands it to your Klaviyo account as an event, and a flow you own sends the email from your domain, in your voice. This section is the whole setup. It takes about five minutes.

When this happens

Almost every winner sees their code on screen the moment they win. Occasionally a code cannot be created at that moment: your store connection is briefly down, or the campaign end date on the popup has already passed. Ignis tells the shopper the code is on its way, records the debt, and creates the code a few minutes later, or as soon as you fix the date. That code has to reach an inbox, and your Klaviyo flow is what delivers it.

Without the flow, a late code goes nowhere. Ignis will create the code and hand it to Klaviyo, and Popup Lab will show that it did. If no flow is live on the metric below, Klaviyo records the event and sends nothing. Build the flow before you run a popup that hands out codes.

What Ignis sends to Klaviyo

One event per late code, on the shopper's profile, using the Klaviyo connection in Settings, then Connectors. The connected key needs permission to write events and to read and write profiles.

WhatValue
Metric nameIgnis: popup code ready
event.codeThe discount code. Single use, unique to this shopper.
event.prize_labelThe prize as named in Popup Lab, for example 15% OFF.
event.expires_atThe last day the code works, as YYYY-MM-DD. Only present when the code has an expiry, so a template can test for it.
event.popup_nameThe popup's name in Popup Lab.
event.experiment_keyThe popup's key. Useful for a flow filter if you run several popups.

Each event carries an identifier Klaviyo uses to discard duplicates, so a retry on our side can never fire your flow twice for the same code. The shopper's code is also written onto their profile as person|lookup:'popup_prize_code', as it is for every winner, so an existing template that reads the profile keeps working.

Step 1. Send a test event

A custom metric only appears in Klaviyo's trigger list after Klaviyo has received it once. So the first step is to send yourself a test.

  1. Confirm Klaviyo is connected in Ignis under Settings, then Connectors.
  2. Open the popup in Popup Lab, then Codes, and click Send a test code to Klaviyo. Ignis sends one event to the Klaviyo profile that matches your own Ignis login email, with the code TEST-CODE-0000 and the prize Test prize. It is not a real discount and cannot be redeemed.
  3. In Klaviyo, open Analytics, then Metrics and find Ignis: popup code ready. It can take a few minutes to appear. If you do not see it, check that the Klaviyo key connected to Ignis can write events.

Step 2. Create the flow

  1. In Klaviyo, open Flows and click Create flow, then Build your own.
  2. Name it, for example Popup code ready.
  3. For the trigger, choose Metric and select Ignis: popup code ready.
  4. Do not add a time delay. The shopper was already told the code is on its way.
  5. Add an Email action and paste the template below, or build your own using the variables in the table above.
  6. Leave the flow in Manual or Draft while you test. Set it to Live in step 4.

Flow filters. Leave them off. A filter such as "has placed an order zero times" or a smart-sending window can stop a shopper who is owed a code from receiving it. This email is transactional in purpose: the shopper asked for the code and was promised it. If you want Klaviyo to treat it that way, mark the email as transactional in the flow settings and follow Klaviyo's rules for doing so.

Step 3. The email template

Plain and brand neutral on purpose. Change the words to match your store. Keep the two variables that carry the code and the prize.

Subject line

Your {{ event.prize_label }} code

Body (paste into a text block; the {% if %} lines show the expiry only when there is one)

Thanks for waiting.

Your {{ event.prize_label }} code could not be created when you entered, and it is ready now.

{{ event.code }}

Use it at checkout.
{% if event.expires_at %}
Use it by {{ event.expires_at }}.
{% endif %}
Need help? Just reply to this email.

If you prefer one line that never breaks, {{ event|lookup:'code'|default:'' }} is the same value written the long way. Property names here have no spaces, so the short form works everywhere in Klaviyo's editor, including the subject line.

Step 4. Test it end to end

  1. In the flow, open the email and use Preview. Choose a recent event to preview with; the test event you sent in step 1 will be there, and the code should render as TEST-CODE-0000.
  2. Set the flow to Live.
  3. Back in Popup Lab, click Send a test code to Klaviyo again. Within a few minutes the email arrives at your Ignis login address, from your Klaviyo sending domain.
  4. If it does not arrive, open your own profile in Klaviyo. The event will be in its activity, and Klaviyo's flow analytics will say why the message was skipped: the flow was not live, a filter removed you, or the profile is suppressed.

What Popup Lab shows you

Under Codes, the code supply card tells you two things about this hand-off. Codes are being handed to Klaviyo, with the time of the most recent one, means Klaviyo has accepted at least one code-ready event for this popup. Make sure the flow is live. No flow has received a code yet means no late code has been needed so far, which is normal for a healthy store. Ignis can see that Klaviyo accepted the event. It cannot see whether your flow sent the email, so if a shopper says nothing arrived, the flow's own analytics in Klaviyo are the place to look.

If Klaviyo is not connected, Ignis will not create a late code at all, because there would be no way to send it. The shopper's debt stays visible in Popup Lab and you are told what to connect.