Work

/

About
Play
Blog
Contact
Home/Blog/Engineering
Engineering
•
Updated Oct 10, 2026
•
4 min read

Building an Embeddable React Widget That Works on Any Site

Building an Embeddable React Widget That Works on Any Site
MS
Muhammad Saad

Shopify engineer. I build storefronts, two published Shopify apps, and the infrastructure behind a 70,000+ product store, and I write here about what that work teaches me.

Follow on LinkedInSee my workBook a call
Found this useful? Share it.

Keep reading

Diagram of chat state, a streamed response and a separate action boundary
Engineering
•
4 min read

React AI Chat: Streaming State, Cancellation and Safe Actions

Build a React chat response state machine with request IDs, UTF-8 stream decoding, cancellation and a separate authorization boundary for store actions.

Jun 20, 2026
Custom React hooks diagram showing reusable subscriptions and cleanup
Engineering
•
4 min read

Custom React Hooks That Survive SSR and Cleanup

Build React media-query hooks that survive server rendering, then test subscription cleanup, changing inputs and delayed widgets before shipping.

Jun 16, 2026
React performance workflow: reproduce, measure and verify a catalog interaction
Engineering
•
5 min read

React Performance: Diagnose Slow Catalog Interactions

Find whether a slow React catalog interaction comes from requests, rendering or layout, then choose a focused fix and verify it with a repeatable trace.

Jan 10, 2026
Back to the blog

© 2026 Muhammad Saad • Colophon

Connect with me on LinkedIn

Elsewhere

  • Github
  • Testimonials
  • CV
  • LinkedIn

Contact

  • Contact details
  • Book a call
  • Email
  • Privacy policy
  • Editorial policy
On this page
  1. Why Embedding React Is Harder Than It Looks
  2. 1. Ship One Script, Mount Your Own Root
  3. 2. Isolate Your CSS From the Host With Shadow DOM
  4. 3. Keep the Bundle Small and Self-Contained
  5. 4. Talk to the Host Page Safely
  6. 5. Configure Each Install With data- Attributes
  7. An Embeddable Widget Checklist
  8. Final Take

An embeddable React widget has to load from one script tag, mount its own DOM, isolate its styles from the host, stay small, and talk to the page without ever assuming control of it. Get those five things right and your widget runs cleanly on any site.

Quick answer

Ship the widget as a single self-executing bundle. On load, create your own container, attach a Shadow DOM, render React into the shadow root, inline your CSS so it cannot leak in or out, read configuration from data- attributes on the script tag, and communicate with the host through events rather than shared globals.

Why Embedding React Is Harder Than It Looks

In your own app, you control the HTML, the CSS reset, the bundler, and the global scope. A widget gives all of that up. The host page may already run React, jQuery, Tailwind, or a CSS framework that fights yours. Their reset may restyle your buttons. Their build has no idea yours exists.

The real problems of a third-party widget
ProblemWhat goes wrongThe fix
CSS collisionsHost styles leak into your widget and yours leak outRender inside a Shadow DOM with inlined styles
Global scopeTwo Reacts, clashing variables, polluted windowOne self-contained bundle, no shared globals
MountingNo guaranteed div to render intoCreate your own container at runtime
Bundle sizeA heavy script slows the host pageCode-split, lazy-load, and keep the loader tiny
ConfigurationEvery site needs different keys and optionsRead data- attributes from the script tag

1. Ship One Script, Mount Your Own Root

The install should be a single line the merchant pastes once. Everything else happens at runtime: the script finds itself, creates a container, and renders React into it. Never assume the page has a div waiting for you.

install snippet (host site)
<script
  src="https://cdn.yoursite.com/widget.js"
  data-widget-key="pk_live_123"
  data-accent="#10312a"
  defer
></script>
widget.js — entry + mount
import { createRoot } from 'react-dom/client';
import Widget from './Widget';

function mount() {
  const script =
    document.currentScript ||
    document.querySelector('script[data-widget-key]');

  // create our own host element, never trust the page to provide one
  const host = document.createElement('div');
  host.id = 'dahlia-widget-host';
  document.body.appendChild(host);

  const shadow = host.attachShadow({ mode: 'open' });
  const mountPoint = document.createElement('div');
  shadow.appendChild(mountPoint);

  createRoot(mountPoint).render(<Widget config={script.dataset} />);
}

if (document.readyState !== 'loading') mount();
else document.addEventListener('DOMContentLoaded', mount);

2. Isolate Your CSS From the Host With Shadow DOM

Shadow DOM is the single most important decision for a widget. Styles inside a shadow root do not leak out, and the host page styles do not leak in. That means your widget looks the same on a Tailwind store, a WordPress theme, or a hand-rolled CSS site.

The catch: your CSS must live inside the shadow root, so a normal stylesheet link will not reach it. Inline the CSS as a string and inject it into the shadow tree. With Vite you can import the compiled CSS as text.

inject inlined CSS into the shadow root
import css from './widget.css?inline';

function injectStyles(shadow) {
  const style = document.createElement('style');
  style.textContent = css;
  shadow.appendChild(style);
}

// call injectStyles(shadow) right after attachShadow,
// before rendering React into the mount point

3. Keep the Bundle Small and Self-Contained

A widget that adds 400 KB to someone else’s checkout will get removed. Treat bundle size as a feature, not an afterthought.

  • Build to a single IIFE/UMD file so there is nothing for the host to configure.
  • Lazy-load the heavy UI only after the launcher button is clicked, so the initial script stays tiny.
  • Avoid large dependencies; date and icon libraries add up fast inside a widget.
  • Bundle React with the widget rather than expecting the host to provide it — version conflicts are not worth the saved kilobytes.
  • Serve from a CDN with long cache headers and a versioned filename.

For the loading boundary itself, see my Shopify search widget loader and failure checklist. It includes a standalone example that preserves the current query while the interface arrives.

4. Talk to the Host Page Safely

Your widget will need to react to the host (cart updated, route changed) and tell the host things (lead captured, conversation opened). Do it through a narrow, explicit interface — custom events or postMessage — never by reaching into the host’s variables.

  • Emit namespaced CustomEvents on window so the host can listen without coupling to your internals.
  • For cross-origin iframes, use postMessage with a strict origin check on both sides.
  • Expose a tiny public API on a single global, like window.Dahlia.open(), instead of many loose functions.
  • Validate every message you receive; treat the host as untrusted input.

5. Configure Each Install With data- Attributes

The same bundle ships to every customer, so configuration has to ride along with the install. Reading data- attributes off the script tag keeps setup to one line and means no separate config file to get out of sync.

read config from the script dataset
function Widget({ config }) {
  const accent = config.accent || '#10312a';
  const apiKey = config.widgetKey;

  // accent and apiKey came straight from data- attributes,
  // so one bundle can theme itself per site
  return (
    <Launcher accent={accent} apiKey={apiKey} />
  );
}

An Embeddable Widget Checklist

Before you ship
AreaQuestion to answer
InstallIs it truly one script tag with sensible defaults?
IsolationDoes it render in a Shadow DOM with inlined CSS?
FootprintIs the initial payload small, with heavy UI lazy-loaded?
SafetyDoes it avoid host globals and validate every message?
ConfigCan a site theme and key it without a code change?
CleanupDoes it unmount and remove its host node on teardown?

Final Take

A good embeddable widget feels invisible to the developer who installs it: one line, no conflicts, no layout surprises. That polish comes from treating the host page as an environment you respect — isolated styles, a small footprint, an explicit API, and clean teardown.

Once that foundation is in place, the React part is the easy part. The architecture is what makes a widget something people are willing to put on their own site.

For the commerce context, see the Dahlia AI sales concierge. For the conversation UI, continue with React chat state and streaming.

If you are deciding how much of a storefront to replace, use the Shopify theme vs headless worksheet to compare a contained widget with a full storefront build.