Integrations

Virtual Try-On for Headless Shopify (Hydrogen)

How to embed try-on in a Hydrogen storefront without breaking hydration or Core Web Vitals

On this page
  1. Why Standard Embed Scripts Break in Hydrogen
  2. A Component-Based Integration Pattern
  3. Handling Hydration Without Layout Shift
  4. Keeping the Product Page's Performance Intact
  5. Where YourSizer Fits
  6. Frequently Asked Questions
  7. Conclusion

Shopify's default theme integrations assume a Liquid template and a <script> tag dropped into theme.liquid. Hydrogen storefronts have neither — they're React Router apps running on Oxygen, with server-side rendering and hydration boundaries that most embeddable widgets were never built for. Get the integration wrong and you either break the page for crawlers and slow-connection users, or ship a try-on button that flashes, double-renders, or never mounts at all.

Headless Shopify adoption has grown quickly among mid-market and enterprise fashion brands that want more control over storefront performance and design than Liquid themes allow. Hydrogen, Shopify's official React framework for this, pairs with Oxygen hosting and Remix-style server rendering. That architecture is great for speed and flexibility, but it changes the rules for any third-party widget — including a virtual try-on or size recommendation experience — that assumes a plain HTML page with jQuery available on window.

Why Standard Embed Scripts Break in Hydrogen

A typical ecommerce widget ships as a single <script src="..."> tag that self-mounts by querying the DOM once the page loads. That pattern depends on two things Hydrogen doesn't guarantee: a stable, fully-rendered DOM at script execution time, and a single client-side render pass.

Hydrogen renders product pages on the server first, streams HTML to the browser, then hydrates React on the client to attach interactivity. If a try-on script queries document.querySelector('#product-image') before hydration completes, it can attach to a DOM node that React is about to replace, producing a detached widget or a duplicate mount. Worse, if the script runs during SSR (because it wasn't guarded), it will throw immediately since window and document don't exist on the server.

The fix isn't exotic — it's the same discipline any React developer applies to third-party DOM-dependent code — but it does need to be deliberate. Treat the try-on experience as a client-only React component, not a script tag, and mount it inside Hydrogen's existing component tree rather than bolting it onto the outside.

A Component-Based Integration Pattern

Rather than injecting a script, wrap the try-on button and modal in a dedicated component that Hydrogen's product route renders directly:

// app/components/TryOnButton.client.tsx
import { Suspense, lazy } from 'react';

const TryOnWidget = lazy(() => import('./TryOnWidget'));

export function TryOnButton({ productId, imageUrl }) {
  return (
    <Suspense fallback={<button disabled>Loading try-on…</button>}>
      <TryOnWidget productId={productId} imageUrl={imageUrl} />
    </Suspense>
  );
}

A few things matter here. The .client.tsx suffix (or an equivalent ClientOnly wrapper if the project uses one) tells the framework this component should not attempt to render on the server. lazy() defers loading the widget's JavaScript bundle until it's actually needed, which keeps it out of the critical rendering path for shoppers who never click the try-on button. Suspense gives you a real fallback state instead of a layout shift when the bundle finishes loading.

This pattern also means the try-on trigger renders as part of the normal product page markup — same position every time, no waiting for a separate script to find and modify the DOM after the fact. The try-on experience becomes just another React component in the tree, not a foreign object bolted onto it.

Handling Hydration Without Layout Shift

Layout shift is the most common visible symptom of a bad headless integration. If the try-on button's container has no reserved size before the component mounts, the page jumps when it appears — hurting both the shopper experience and Cumulative Layout Shift, one of Google's Core Web Vitals.

The reliable fix is to reserve space with CSS before any JavaScript runs, rather than letting the button's real dimensions determine layout after the fact:

ApproachLayout shift riskNotes
Script-injected button, no placeholderHighCommon cause of CLS penalties on PDPs
Fixed-height container + lazy componentLowReserve the pixel height in CSS before mount
Server-rendered placeholder + client swapLowestRequires a static, non-interactive SSR fallback

For most Hydrogen storefronts, a fixed-height flex container around the Suspense boundary is enough — you don't need full SSR of the try-on UI itself, since the button and modal are inherently interactive and add little to the server-rendered HTML that search engines index.

Keeping the Product Page's Performance Intact

Oxygen and Hydrogen are built around fast Time to First Byte and aggressive caching, and it's easy to undo that advantage with a poorly scoped third-party bundle. A few practical guardrails:

  • Load the try-on bundle only on product and collection routes, not globally in the root layout, so pages without a "try it on" entry point don't pay the download cost.
  • Defer image/model asset fetching (camera access, body model files, garment renders) until the shopper actually opens the try-on modal, not on page load.
  • Confirm the widget doesn't register global event listeners that survive route changes — Hydrogen's client-side routing means components mount and unmount far more often than in a traditional multi-page Liquid theme, and a leaked listener compounds over a session.

This last point trips up integrations built for static sites: they assume the page reloads between products, so cleanup was never a concern. In a single-page app it is.

Where YourSizer Fits

YourSizer's widget is built as a set of embeddable React/JS components, which maps naturally onto Hydrogen's component model instead of requiring a global script injection. For a Hydrogen storefront, that typically means importing the try-on and size recommendation trigger as a client-only component on the product route, passing in the product ID and image reference as props, and letting it lazy-load its own bundle behind a Suspense boundary — the same pattern described above.

Because the integration doesn't depend on Liquid template hooks, it works the same way whether the storefront is on Oxygen, Vercel, or another hosting target for the headless build. Teams evaluating a broader size recommendation solution for a headless migration generally find the technical integration is the easy part — the harder work is deciding where in the shopping flow (PDP, cart, or both) the try-on entry point should live, which is a merchandising decision, not an engineering one.

Frequently Asked Questions

Does virtual try-on slow down a Hydrogen storefront's Core Web Vitals?

It doesn't have to, if the integration is scoped correctly. The main risk to Largest Contentful Paint and Cumulative Layout Shift comes from loading the full try-on bundle eagerly on every page load and from not reserving layout space for the trigger button. Lazy-loading the component behind a route-specific import, and reserving a fixed container size in CSS, keeps the try-on feature from affecting the metrics Google uses for page experience signals.

Can a try-on widget be server-side rendered in Hydrogen?

The interactive parts — camera capture, garment rendering, size logic — generally cannot be meaningfully server-rendered, since they depend on browser APIs and client state. What can and should be server-rendered is the static shell around it: the button label, its position in the layout, and any structured product data. Treating the interactive widget as a client-only component while letting the surrounding page render normally on the server is the standard approach.

Is a headless Shopify migration a good time to add try-on or size recommendation tools?

It's a reasonable time to plan the integration, since the team is already touching the product page templates and choosing component architecture. Some brands prefer to launch the headless storefront first with feature parity to the existing theme, then add try-on as a follow-up once the new architecture is stable. Either order works; what matters is that the integration is designed as a native component rather than ported over as a Liquid-era script tag, which is where most of the friction described above originates.

How does this differ from adding try-on to a standard Shopify Liquid theme?

On a Liquid theme, the integration is usually a script tag and a snippet inserted into product.liquid, with the widget locating its mount point via DOM queries after the page loads. In Hydrogen, the equivalent mount point is a React component reference passed explicit props, and the framework's own rendering lifecycle — not a DOMContentLoaded listener — determines when and how the widget appears. The end-user experience can look identical; the underlying integration contract is different.

Conclusion

Headless Shopify rewards teams that treat every storefront feature, including try-on, as a first-class piece of the component tree rather than an afterthought bolted onto the page. The technical requirements — client-only rendering, lazy loading, layout reservation — are neither exotic nor difficult, but they do need to be handled deliberately during the Hydrogen build rather than retrofitted later. Brands that get this right end up with a try-on experience that loads fast, doesn't shift the page, and fits as naturally into the storefront as any other React component, which is ultimately what a headless migration was supposed to deliver in the first place.

TopicsShopify HydrogenHeadlessVirtual try-on

Put it into practice

See YourSizer on your own store

Size recommendation, 3D body model and photo try-on — live on your product pages in a day.