DEV Community

Sazzad Hossain
Sazzad Hossain

Posted on

Rendering JSON-Driven Popups in a Shopify Theme App Extension Without React 🤖

The problem

I needed merchant-designed popup layouts to render inside a Shopify storefront, even though the editor and its React components live in the app admin. The extension receives popup content as JSON, so the storefront had to turn an element tree into real DOM nodes without shipping the admin UI or showing a half-built popup.

Context

The theme app block creates empty overlay containers and loads a set of deferred assets. Popup data comes from a shop metafield, while separate UI modules handle full-page, lightbox, and floating-bar variants. A shared renderer is responsible for converting sections, containers, text, images, inputs, buttons, and forms into DOM elements. It also applies responsive styles and connects special buttons to popup behavior.

What I tried first 😞

The straightforward version rendered each element directly into the visible container as soon as it was created. That kept the code small, but image-heavy layouts could become visible before their assets were ready. The result was a popup whose dimensions and content changed after opening. A loading placeholder in Liquid did not solve the underlying problem: it only hid the empty state, not the incomplete final state.

The fix 💯

I kept the recursive renderer, but changed the outer render operation into a preparation phase. It builds the complete tree in a detached host, waits for pending images, then moves the finished nodes into the visible container:

`const fragment = document.createDocumentFragment();
elements.forEach((data) => {
  const element = renderElement(data, options);
  if (element) fragment.appendChild(element);
});

const preloadHost = document.createElement("div");
preloadHost.setAttribute("aria-hidden", "true");
preloadHost.appendChild(fragment);
document.body.appendChild(preloadHost);

await Promise.allSettled(
  collectPendingImages(preloadHost).map(waitForImageReady),
);

container.innerHTML = "";
while (preloadHost.firstChild) container.appendChild(preloadHost.firstChild);
preloadHost.remove();`
Enter fullscreen mode Exit fullscreen mode

The UI modules now await renderElements before running their rendered callbacks, so event handlers attach after content preparation. The same renderer serves all popup variants, while popupData and instance state provide context for conditional inputs, reward-code substitution, phone enhancements, and responsive layout patches.

That boundary keeps rendering concerns centralized while each variant remains responsible for its own overlay lifecycle and visibility rules.

Takeaway

For JSON-driven storefront UI, separate element construction from visible insertion. Render and prepare off-screen, wait for layout-sensitive assets, then commit one complete DOM update. This keeps a flexible schema-driven UI from exposing its assembly process to customers.

Have you built a schema-driven renderer for an environment where your main UI framework could not run?

Top comments (0)