Embed loader
Two lines of HTML and your customers can try on a product from any browser. The loader registers a <tryonar-button> custom element that opens a fullscreen try-on iframe; an event surface lets you wire the rest of your site.
Quickstart
Paste these two snippets anywhere in your page’s HTML. That’s the whole install.
<script type="module"
src="https://cdn.tryonar.net/widget.js"
defer></script>
<tryonar-button product-id="prod_YOUR_PRODUCT_ID"></tryonar-button>The button renders inline where you put the element, inheriting your page’s font. Click it: a fullscreen modal opens with the try-on for your product.
The script is ~13 KB gzipped, ships from a CDN with stale-while-revalidate caching, and re-entrant (including it twice is a no-op).
Configuration
Every option is a plain HTML attribute on the <tryonar-button> element. Set them in your markup; updates take effect on the next render.
| Attribute | Type | Default | Purpose |
|---|---|---|---|
product-id | string (required) | — | TryonAR product ID, format prod_… |
label | string | Try on & find my shade | Button text override |
mode | inline | floating | inline | Inline = render where the element is. Floating = fixed to a corner. |
floating-position | bottom-right | bottom-left | bottom-right | Only used when mode=floating |
theme | auto | light | dark | auto | Auto follows prefers-color-scheme |
accent-color | hex string | — | Override the button background with your brand color |
debug | boolean | false | Logs click + lifecycle to the console |
Events
Subscribe via window.TryonAR.on(name, handler) and unsubscribe with .off(name, handler). Payloads are typed; see the table below for the shape of each.
window.TryonAR.on('cart.add', ({ productId, variantId }) => {
// wire your analytics or your own cart logic here
});| Event | Payload | When it fires |
|---|---|---|
button.click | { productId } | Before the modal opens. Cancel via the DOM tryonar:open CustomEvent. |
modal.open | { productId } | Modal mounted; iframe loading. |
widget.ready | { productId, protocolVersion } | Iframe handshake completed; widget UI is interactive. |
cart.add | { productId, variantId?, size?, metadata? } | Add-to-cart succeeded via at least one bridge. |
cart.error | { productId, message } | Add-to-cart failed across every applicable bridge. |
iframe.error | { productId, code, message } | Iframe load failed or widget posted an error. |
iframe.analytics | { event, props? } | Forwarded analytics from inside the widget (e.g. shade swaps). |
modal.close | { productId } | Any close path: ✕, ESC, backdrop click, browser back, cart-add success. |
Cart bridges
When the user taps “Add to bag” inside the try-on, the loader runs two bridges in order:
Shopify
Detected automatically when window.Shopify.shop exists (every standard Shopify storefront theme). The loader POSTs to /cart/add.js with the variant ID resolved from the metadata.shopifyVariantId field on the TryonAR variant. On a 422 (out-of-stock, etc.) the modal stays open and shows the merchant-facing error inline.
Hydrogen / headless stores don’t expose window.Shopify; the universal CustomEvent path below still fires and you can wire your own cart there.
Universal CustomEvent
Always fires after the platform-specific bridges run, so analytics consumers see every add-to-cart attempt regardless of platform. Listen on window:
window.addEventListener('tryonar:addtocart', (e) => {
const { productId, variantId, size, metadata } = e.detail;
// wire to your backend cart
});Framework notes
Plain HTML
See Quickstart above.
React
React 18+ supports custom-element attributes natively. Cast as any on the JSX element if your TS config doesn’t know about tryonar-button yet.
import Script from 'next/script';
export function TryOnButton({ productId }: { productId: string }) {
return (
<>
<Script src="https://cdn.tryonar.net/widget.js" strategy="afterInteractive" />
{/* @ts-expect-error - custom element is registered at runtime */}
<tryonar-button product-id={productId} />
</>
);
}Vue 3
Add tryonar-button to your Vue compiler’s isCustomElement list:
// vite.config.ts
export default defineConfig({
plugins: [vue({
template: { compilerOptions: { isCustomElement: tag => tag === 'tryonar-button' } }
})],
});<template>
<tryonar-button :product-id="productId" />
</template>Astro
Drop the <script> in the layout’s <head> with is:inline. The custom element works in any template.
Versioning
Two URL forms:
- Stable:
https://cdn.tryonar.net/widget.js— what most merchants use. Edge-cached for 5 minutes; we can push fixes fast. - Pinned:
https://cdn.tryonar.net/v/<version>/widget.js— immutable, cached forever. Use this if you have a strict change-management process; subscribe to our release notes for when to bump.
The protocol version (host ↔ iframe) is independent of the loader version and follows semver. Major bumps are documented in release notes; minor bumps are additive.
Troubleshooting
The button renders but the modal won’t open
Check the browser console for a tryonar:open event being cancelled. If a host-page handler calls preventDefault(), the modal won’t mount. Drop debug on the button to see the click hit the loader.
Modal opens but spinner never clears
After 2 seconds the iframe will retry the handshake; after another 2 seconds it shows a “Connection lost” screen. The most common cause is a Content-Security-Policy that blocks the iframe’s origin — add frame-src https://try.tryonar.net to your CSP.
Add-to-cart doesn’t hit my cart
On Shopify: confirm window.Shopify.shop is set (it is on standard themes; missing on Hydrogen and most headless stacks). On any platform, listen for the universal tryonar:addtocart CustomEvent on window — it always fires.
The button looks wrong on my page
The button lives in shadow DOM; your CSS doesn’t leak into it. Customize via the theme and accent-color attributes — full theming is on the roadmap.