Skip to content

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.

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.

  1. Install the TryOn app and finish onboarding — the snippet talks to your shop through the app.
  2. 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.
  3. Duplicate your theme before editing. You’re editing theme code; a copy is your undo.

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.liquid
  • sections/product.liquid
  • templates/product.liquid

Search the file for Add to cart — the file that contains the add-to-cart button is the one you want.

Paste this immediately after the add-to-cart button, then save.

sections/product-template.liquid
{%- 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 -%}

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.

  1. Open the storefront product page of a product you enabled try-on for. The button appears below add-to-cart.
  2. Open any product you have not enabled. There should be no button and no empty gap.
  3. Click the button — the try-on modal opens after a short load.
  4. 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.
  5. Check the dashboard the next day — Views should be counting.

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).

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.

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.

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.

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
  • 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.