Install on a custom or vintage theme
The Try On button block needs an Online Store 2.0 theme. If your theme is a vintage (pre-2.0) theme, or a heavily customised one whose product page can’t host app blocks, paste the snippet on this page into your product template instead.
The snippet does the same job as the app block: it renders a button on products you’ve enabled try-on for, loads the try-on script when a shopper clicks it, and reports the same impression events to your dashboard.
Do you actually need this?
Section titled “Do you actually need this?”Open Online Store → Themes → Customize and select a product page. If the product section has an Add block button, your theme supports app blocks — use the Try On button block and stop here. It’s configurable from the theme editor, it survives theme updates, and we keep it current.
Use the snippet only when Add block isn’t there.
Before you start
Section titled “Before you start”- Install the TryOn app and finish onboarding — the snippet talks to your shop through the app.
- Enable try-on on at least one product — see Enable try-on on a product. The snippet renders nothing on products that aren’t enabled, so without one you can’t tell whether it worked.
- Duplicate your theme before editing. You’re editing theme code; a copy is your undo.
Step 1 — Find your product template
Section titled “Step 1 — Find your product template”In Online Store → Themes → ⋯ → Edit code, find the file that renders the product page. On most vintage themes it’s one of:
sections/product-template.liquidsections/product.liquidtemplates/product.liquid
Search the file for Add to cart — the file that contains the add-to-cart button is the one you
want.
Step 2 — Paste the snippet
Section titled “Step 2 — Paste the snippet”Paste this immediately after the add-to-cart button, then save.
{%- comment -%} TryOn — "See it on you" button for themes that can't use app blocks.
Paste this into your product template, right after the Add to cart button. The button renders only on products you have enabled try-on for; on every other product it outputs nothing at all.{%- endcomment -%}
{%- comment -%} ---------- Settings you can change ---------- {%- endcomment -%}{%- assign tryon_button_text = "See it on you" -%}{%- assign tryon_button_color = "#000000" -%}{%- assign tryon_button_text_color = "#ffffff" -%}{%- assign tryon_button_radius = "8px" -%}{%- assign tryon_show_helper_text = true -%}{%- assign tryon_api_base_url = "https://api.integration.tryonvirtual.com" -%}{%- comment -%} -------------------------------------------- {%- endcomment -%}
{%- if product and product.metafields.tryon.has_asset.value == true -%} {%- assign tryon_model_type = product.metafields.tryon.model_type.value -%} {%- if tryon_model_type == 'swap' -%} {%- assign tryon_helper_text = "Not sure if it's for you? Upload a photo and see yourself in it." -%} {%- else -%} {%- assign tryon_helper_text = "Not sure if it's for you? Point your camera. See yourself in it." -%} {%- endif -%}
{%- comment -%} Selectors are scoped to two element ids on purpose. Themes and page builders ship button resets that outrank a single class and silently strip the styling below. {%- endcomment -%} {%- assign tryon_wrap = "#tryon-vt-" | append: product.id -%} {%- assign tryon_btn = tryon_wrap | append: " #tryon-vt-btn-" | append: product.id -%} {%- assign tryon_help = tryon_wrap | append: " #tryon-vt-helper-" | append: product.id -%}
<style> {{ tryon_wrap }} { display: flex; flex-direction: column; align-items: stretch; gap: 8px; margin: 12px 0; } {{ tryon_btn }} { background: {{ tryon_button_color }} !important; color: {{ tryon_button_text_color }} !important; border: none !important; padding: 12px 24px !important; font-size: 16px !important; font-weight: 500 !important; border-radius: {{ tryon_button_radius }}; width: 100%; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: 8px; font-family: inherit; line-height: 1.4; cursor: pointer; transition: opacity .2s ease; } {{ tryon_btn }}:hover:not(:disabled) { opacity: .88; } {{ tryon_btn }}:disabled { opacity: .5; cursor: not-allowed; } {{ tryon_btn }}.is-loading { position: relative; color: transparent !important; } {{ tryon_btn }}.is-loading::after { content: ""; position: absolute; width: 20px; height: 20px; border: 2px solid {{ tryon_button_text_color }}; border-top-color: transparent; border-radius: 50%; animation: tryon-vt-spin .8s linear infinite; } {{ tryon_help }} { margin: 0; font-size: 13px !important; color: #333333 !important; line-height: 1.4; } @keyframes tryon-vt-spin { to { transform: rotate(360deg); } } </style>
<div class="tryon-vt-container" id="tryon-vt-{{ product.id }}"> <button type="button" class="tryon-vt-button" id="tryon-vt-btn-{{ product.id }}" data-tryon-shop="{{ shop.permanent_domain | default: shop.domain }}" data-tryon-product-id="{{ product.id }}" data-tryon-variant-id="{{ product.selected_or_first_available_variant.id }}" data-tryon-cart-add-url="{{ routes.cart_add_url | default: '/cart/add' }}" data-tryon-cart-url="{{ routes.cart_url | default: '/cart' }}" data-tryon-locale="{{ request.locale.iso_code }}" aria-label="{{ tryon_button_text | escape }}" > <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path> <circle cx="12" cy="12" r="3"></circle> </svg> <span>{{ tryon_button_text }}</span> </button> {%- if tryon_show_helper_text -%} <p class="tryon-vt-helper" id="tryon-vt-helper-{{ product.id }}">{{ tryon_helper_text }}</p> {%- endif -%} </div>
<script> (function () { 'use strict';
var button = document.getElementById('tryon-vt-btn-{{ product.id }}'); if (!button) return;
var apiBaseUrl = '{{ tryon_api_base_url }}'; var shopDomain = button.getAttribute('data-tryon-shop'); var productId = button.getAttribute('data-tryon-product-id'); var locale = button.getAttribute('data-tryon-locale') || '';
// Vintage themes swap variants without reloading the page, but they do // update ?variant= in the URL — prefer that over the value rendered at // build time so add-to-cart from inside try-on uses the right variant. function currentVariantId() { try { var fromUrl = new URLSearchParams(window.location.search).get('variant'); if (fromUrl) return fromUrl; } catch (e) {} return button.getAttribute('data-tryon-variant-id') || ''; }
function publishConfig() { window.TryOnShopifyConfig = window.TryOnShopifyConfig || {}; window.TryOnShopifyConfig[productId] = { shopDomain: shopDomain, productId: productId, variantId: currentVariantId(), cartAddUrl: button.getAttribute('data-tryon-cart-add-url'), cartUrl: button.getAttribute('data-tryon-cart-url'), hasAsset: true, apiBaseUrl: apiBaseUrl, shopifyRoutes: window.Shopify && window.Shopify.routes ? window.Shopify.routes : null }; } publishConfig();
function bootstrapUrl() { return apiBaseUrl + '/api/v1/tryon/scripts/embed/bootstrap.js' + '?shop=' + encodeURIComponent(shopDomain) + '&productId=' + encodeURIComponent(productId) + '&variantId=' + encodeURIComponent(currentVariantId()) + '&locale=' + encodeURIComponent(locale); }
/* ---------- Impression analytics (no personal data) ------------------ Two beacons feed the conversion funnel in your dashboard: pdp_viewed - this product page rendered a try-on button button_viewed - that button actually scrolled into view Both are gated on the shopper's analytics consent. Remove this section and the Views and Engagement rate figures on your dashboard stay at zero, even when shoppers are using try-on. --------------------------------------------------------------------- */ var analyticsUrl = apiBaseUrl + '/api/v1/tryon/analytics/events';
function tryonUuid() { try { if (window.crypto && crypto.randomUUID) return crypto.randomUUID(); } catch (e) {} return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (ch) { var r = (Math.random() * 16) | 0; return (ch === 'x' ? r : (r & 0x3) | 0x8).toString(16); }); }
var impressionSessionId = tryonUuid();
function sendImpression(eventName, visitorId) { try { var body = JSON.stringify({ shop_domain: shopDomain, events: [{ event_name: eventName, event_id: tryonUuid(), session_id: impressionSessionId, visitor_id: visitorId, product_id: productId, variant_id: currentVariantId(), source: 'pdp', timestamp: new Date().toISOString(), metadata: {} }] }); var blob = new Blob([body], { type: 'text/plain' }); if (!(navigator.sendBeacon && navigator.sendBeacon(analyticsUrl, blob))) { fetch(analyticsUrl, { method: 'POST', body: body, keepalive: true }).catch(function () {}); } } catch (e) {} }
function startImpressionTracking() { var visitorId = ''; try { visitorId = localStorage.getItem('tryon_visitor_id') || ''; if (!visitorId) { visitorId = tryonUuid(); localStorage.setItem('tryon_visitor_id', visitorId); } } catch (e) {}
sendImpression('pdp_viewed', visitorId);
if ('IntersectionObserver' in window) { var observer = new IntersectionObserver(function (entries) { for (var i = 0; i < entries.length; i++) { if (entries[i].isIntersecting) { sendImpression('button_viewed', visitorId); observer.disconnect(); break; } } }, { threshold: 0.5 }); observer.observe(button); } else { sendImpression('button_viewed', visitorId); } }
// Only track when the shopper allows analytics processing. try { window.Shopify.loadFeatures([{ name: 'consent-tracking-api', version: '0.1' }], function (err) { if (err) return; try { var cp = window.Shopify && window.Shopify.customerPrivacy; if (cp && cp.analyticsProcessingAllowed()) startImpressionTracking(); } catch (e) {} }); } catch (e) {}
/* ---------- Loading the try-on experience --------------------------- */ var scriptLoaded = false; var scriptLoading = false; var preloaded = false;
function preload() { if (preloaded || scriptLoaded || scriptLoading) return; preloaded = true; var link = document.createElement('link'); link.rel = 'preload'; link.as = 'script'; link.href = bootstrapUrl(); document.head.appendChild(link); } button.addEventListener('mouseenter', preload, { once: true }); button.addEventListener('touchstart', preload, { once: true, passive: true });
function loadBootstrap() { if (scriptLoaded) return Promise.resolve(); if (scriptLoading) { return new Promise(function (resolve, reject) { var waited = 0; var iv = setInterval(function () { if (scriptLoaded) { clearInterval(iv); resolve(); } else if ((waited += 50) >= 10000) { clearInterval(iv); reject(new Error('timeout')); } }, 50); }); } scriptLoading = true; return new Promise(function (resolve, reject) { var s = document.createElement('script'); s.src = bootstrapUrl(); s.async = true; s.onload = function () { scriptLoaded = true; scriptLoading = false; resolve(); }; s.onerror = function () { scriptLoading = false; reject(new Error('failed to load TryOn')); }; document.head.appendChild(s); }); }
function waitForOpenTryOn() { return new Promise(function (resolve, reject) { var attempts = 0; var iv = setInterval(function () { if (typeof window.openTryOn === 'function') { clearInterval(iv); resolve(); } else if (++attempts >= 60) { clearInterval(iv); reject(new Error('timeout')); } }, 100); }); }
function showError(message) { var label = button.querySelector('span'); var original = label.textContent; label.textContent = message; button.disabled = true; setTimeout(function () { label.textContent = original; button.disabled = false; }, 3000); }
button.addEventListener('click', function (e) { e.preventDefault(); if (button.disabled || button.classList.contains('is-loading')) return;
publishConfig(); button.classList.add('is-loading');
loadBootstrap() .then(waitForOpenTryOn) .then(function () { button.classList.remove('is-loading'); window.openTryOn(); }) .catch(function (err) { console.error('TryOn:', err); button.classList.remove('is-loading'); showError('Try On unavailable'); }); }); })(); </script>{%- endif -%}Step 3 — Change the settings
Section titled “Step 3 — Change the settings”The six assign lines at the top are the only ones you need to touch:
| Setting | What it does |
|---|---|
tryon_button_text |
The button label. Keep it short and action-shaped. |
tryon_button_color |
Background colour, any CSS colour value. |
tryon_button_text_color |
Label and icon colour. |
tryon_button_radius |
Corner radius, e.g. 0, 8px, 999px for a pill. |
tryon_show_helper_text |
true or false — the line of copy under the button. |
tryon_api_base_url |
Leave as-is. Only change it if support asks you to. |
To restyle anything else, target .tryon-vt-button in your own theme CSS with !important — the
snippet marks six properties important to survive theme resets, so an ordinary rule of yours
won’t win against them.
The button and helper text are English in the snippet. If your storefront is in another language,
translate the tryon_button_text value and the two tryon_helper_text values directly in the
code — this snippet doesn’t read your theme’s translation files.
Step 4 — Test it
Section titled “Step 4 — Test it”- Open the storefront product page of a product you enabled try-on for. The button appears below add-to-cart.
- Open any product you have not enabled. There should be no button and no empty gap.
- Click the button — the try-on modal opens after a short load.
- On a product with several variants, switch variant, then try on and add to cart from inside try-on. Check the cart holds the variant you picked. If it holds the wrong one, see troubleshooting below.
- Check the dashboard the next day — Views should be counting.
How it decides to render
Section titled “How it decides to render”The snippet reads the tryon.has_asset product metafield, which the TryOn app keeps in sync when
you enable or disable try-on on a product. Nothing else gates it. That’s why:
- Enabling a product in the app makes the button appear with no theme edit.
- Disabling one makes it disappear the same way.
- Uninstalling the app leaves the snippet in place but renders nothing, because the metafield stops being maintained.
It reads a second metafield, tryon.model_type, only to pick which line of helper text to show
(AI Swap vs. real-time AR).
Common questions
Section titled “Common questions”Can we change the button colour and the button text?
Section titled “Can we change the button colour and the button text?”Yes — both are single-line edits at the top of the snippet, and neither needs anything else touched:
{%- assign tryon_button_text = "Try it on" -%}{%- assign tryon_button_color = "#1a1a1a" -%}{%- assign tryon_button_text_color = "#ffffff" -%}tryon_button_color and tryon_button_text_color take any CSS colour value — hex, rgb(), or a
named colour. Save the file and hard-reload a product page to see the change. It applies to every
try-on product at once; there’s no per-product override.
Can the button match our theme’s button colour automatically?
Section titled “Can the button match our theme’s button colour automatically?”Sometimes. Most themes in the Dawn family expose their button colour as a CSS variable, so you can
swap the colour lines in the <style> block for:
background: rgb(var(--color-button, 26,26,26)) !important;color: rgb(var(--color-button-text, 255,255,255)) !important;Keep the fallback values inside var(), exactly as shown. Written any other way, a theme that
doesn’t define those variables renders an invisible button rather than falling back.
Test it immediately after saving. If the button turns invisible or the wrong colour, your theme doesn’t use that convention — put the plain hex values back. This is why the app block ships the same feature switched off by default.
Can we make it look exactly like our Add to cart button?
Section titled “Can we make it look exactly like our Add to cart button?”Add your own rule after the snippet, using !important — the snippet marks six properties
important to survive theme resets, so a plain rule won’t win:
.tryon-vt-button { background: var(--your-theme-button-bg) !important; border-radius: 0 !important; text-transform: uppercase !important;}.tryon-vt-button and .tryon-vt-helper are stable class hooks — they won’t change if we revise
this snippet.
Can we move the button somewhere else on the page?
Section titled “Can we move the button somewhere else on the page?”Yes. The snippet renders where you paste it, so move the whole block to a different point in the product template — above the price, under the description, wherever. Paste it in one place only; two copies on the same page produce two buttons.
Can we hide the line of text under the button?
Section titled “Can we hide the line of text under the button?”Set tryon_show_helper_text to false.
Does it slow the product page down?
Section titled “Does it slow the product page down?”No. The snippet is a small piece of code that adds nothing for your page to download while it loads, so your product pages stay exactly as fast as they are today. Try-on only starts loading when a shopper taps the button — and it gets a head start the moment they hover over it, so it opens straight away.
Do we need to edit the theme again when we enable more products?
Section titled “Do we need to edit the theme again when we enable more products?”No. The snippet checks each product’s try-on status as the page renders, so enabling or disabling a product in the TryOn app is enough. The only reason to touch the theme again is to restyle the button or move it.
Can we show the button on collection or search pages?
Section titled “Can we show the button on collection or search pages?”Not with this snippet — it needs a single product’s context. The storefront badge that decorates product cards is an app embed, which requires an Online Store 2.0 theme.
Will it work on mobile?
Section titled “Will it work on mobile?”Yes. The button is full-width and the try-on experience is built mobile-first — for real-time AR it’s the better device, since phones have a front camera pointed at the shopper.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Likely cause | Fix |
|---|---|---|
| No button on any product | No product is enabled yet, or the metafield hasn’t synced | Enable a product in the app, wait a minute, then hard-reload the storefront page |
| No button on one product | That product isn’t enabled, or has no active asset | Check its Setup progress card in the app |
| Button shows, click does nothing | The bootstrap script failed to load | Open the browser console and look for a TryOn: error |
| Button briefly says “Try On unavailable” | Same as above — the script or product lookup failed | Confirm the product is still enabled; check the console |
| Button is unstyled or the wrong size | A theme or page-builder CSS reset is winning | Add your own rule on .tryon-vt-button with !important |
| Wrong variant added to cart from inside try-on | Your theme changes variants without updating ?variant= in the URL |
Contact support — this needs a theme-specific tweak |
| Dashboard Views stays at 0 | The analytics section was removed, or shoppers haven’t consented to analytics | Re-paste the snippet in full; check your cookie-banner settings |
| Two buttons on one page | The snippet is pasted in two files, or the template renders twice | Remove the duplicate paste |
What you give up versus the app block
Section titled “What you give up versus the app block”- No theme-editor settings. Every change is a code edit.
- No Quick Icons badge. The storefront badge on collection and search pages is an app embed, which also needs Online Store 2.0.
- No automatic updates. The app block improves with each app release; this snippet stays exactly as you pasted it. When we change something that affects it, we’ll update this page — check back after major app updates, or ask support.
- Theme updates can overwrite it. If your theme vendor ships an update to the same template file, re-paste the snippet afterwards.
If you’d rather not edit theme code, the durable fix is moving to an Online Store 2.0 theme — then the app block handles all of this from the theme editor.
- Enable try-on on a product — the button won’t show until you do.
- Where to put the try-on button — placement advice that applies the same way here.
- Troubleshooting — for problems that aren’t specific to this snippet.