guide / react in an existing theme·updated July 28, 2026·~9 MIN READ

Add React to your Shopify theme — without going fully headless.

Two real ways to embed React components inside a live Liquid theme, for the pieces of UI that need it — without rebuilding the whole storefront yet.

This is not full headless

This guide is for adding React inside an existing Liquid theme — one component, one page, one interactive piece. If the goal is replacing the whole frontend, see the full Shopify headless guide instead.

Method 1: Vite-bundled islands

Build a standalone React component with Vite, output a single JS/CSS bundle, and drop it into your theme's assets/ folder. Mount it from a Liquid snippet into a specific div:

// vite.config.ts — build a single-file bundle, no code-splitting
export default defineConfig({
  build: {
    outDir: '../theme/assets',
    rollupOptions: {
      input: 'src/product-configurator.tsx',
      output: { entryFileNames: 'product-configurator.js', format: 'iife' },
    },
  },
});
{% comment %} snippets/product-configurator.liquid {% endcomment %}
<div
  id="product-configurator"
  data-product-id="{{ product.id }}"
  data-variants="{{ product.variants | json | escape }}"
></div>
{{ 'product-configurator.js' | asset_url | script_tag }}
// src/product-configurator.tsx
import { createRoot } from 'react-dom/client';

const el = document.getElementById('product-configurator')!;
const productId = el.dataset.productId!;
const variants = JSON.parse(el.dataset.variants!);

createRoot(el).render(<ProductConfigurator productId={productId} variants={variants} />);

The Liquid template passes product data into the mount point via data-* attributes; React reads it on mount. No API call needed for data the theme already has server-side.

Method 2: script-tag mounting via theme.liquid

For a component that needs to appear across many templates, add the script tag once in theme.liquid and mount conditionally based on what's present on the page:

{% comment %} layout/theme.liquid, before </body> {% endcomment %}
{{ 'react-widgets.js' | asset_url | script_tag }}
// src/react-widgets.tsx — mounts only where its target exists
import { createRoot } from 'react-dom/client';

document.querySelectorAll('[data-react-widget]').forEach((el) => {
  const widget = el.getAttribute('data-react-widget');
  const Component = widgets[widget as keyof typeof widgets];
  if (Component) createRoot(el).render(<Component />);
});

This pattern scales to several independent widgets sharing one bundle, at the cost of shipping slightly more JS on pages that only use one of them. For a single component, Method 1 is simpler.

The honest limits

  • The Liquid-rendered shell still loads first — no edge rendering benefit for the page itself, only for the embedded component.
  • Each mount point ships its own React runtime unless bundles are shared deliberately — watch total JS weight as islands multiply.
  • Data available server-side (product, cart) is easy to pass in via data-* attributes; anything else still needs an API call from the client.
  • This doesn't touch checkout, SEO structure, or Core Web Vitals the way a full headless rebuild does — see the complete guide for that comparison.

Frequently asked questions

Can I add React to my Shopify theme without going fully headless?

Yes. Bundle a React component with Vite (or another bundler) into a single JS/CSS file, drop the compiled output into your theme's assets folder, and mount it into a specific DOM node from a Liquid snippet. The rest of the theme — Liquid templates, checkout, cart — stays exactly as it is.

Does this replace the Shopify theme editor?

No — that's the point. The Liquid theme keeps rendering everything except the specific mount points you've added React components to. Merchants can still use the theme editor for everything outside those islands.

When should I do this instead of going fully headless?

When one or two specific pieces of UI need React's interactivity — a product configurator, a live filter panel, a custom quiz — but a full storefront rebuild isn't justified yet. It's a good way to prove out React on a live site before committing to a full headless migration.

What are the limits of this approach?

You don't get edge rendering, the Liquid theme's server-rendered shell still loads first, and each embedded island ships its own React runtime unless bundles are shared carefully. It's a targeted enhancement, not a performance rewrite — for that, see the full headless guide.

Ready for more than one island?

Once React is handling more than a widget or two, the theme's server-rendered shell becomes the bottleneck. Paste your Shopify URL on the homepage to see the same store rendered fully headless.