Last Update: 25/03/2026
Manual Integration Guide
This guide is intended for developers or merchants using custom or legacy Shopify themes that do not support OS 2.0 "drag & drop" App Blocks. By following this guide, you can manually integrate ARShades VTO, 3D Viewer, and ARPDMeter directly into your theme's Liquid code.
This is an advanced guide that requires editing theme .liquid files. Create a backup of your theme before proceeding.
Prerequisites
Before starting, ensure you have:
- The ARShades Shopify Plugin installed and your license connected.
- Products synchronised — the plugin automatically writes the necessary metafields to your Shop and Variant objects.
- Access to your theme code (Online Store > Themes > Edit code).
Where to insert the code: In your product template file (e.g. sections/main-product.liquid, sections/product-template.liquid, or snippets/buy-buttons.liquid), near the "Add to Cart" button.
Step 1: Read Metafields and Build Variant Maps
The first block reads global shop metafields and loops through product variants to build two JSON-compatible maps:
- VTO map: Associates each Shopify variant ID with its ARShades
catalogueVariantId. - 3D map: Associates each Shopify variant ID with an object containing both
catalogueVariantIdandcatalogueProductId.
If at least one variant has the required metafields, the corresponding feature flag (ars_has_vto / ars_has_3d) is set to true.
{%- comment -%} ===== ARShades VTO + 3D Buttons ===== {%- endcomment -%}
{%- liquid
assign ars_catalogue_id = shop.metafields.arshades.catalogueId
assign ars_web_domain = shop.metafields.arshades.webDomain
assign ars_viewer_id = shop.metafields.arshades.viewerId
assign ars_studio_domain = shop.metafields.arshades.studioDomain
assign ars_lang = request.locale.iso_code | slice: 0, 2
assign ars_has_vto = false
assign ars_has_3d = false
assign ars_current_variant = product.selected_or_first_available_variant
assign ars_vto_parts = ""
for variant in product.variants
if variant.metafields.arshades.catalogueVariantId != blank
assign ars_has_vto = true
assign entry = variant.id | append: '":"' | append: variant.metafields.arshades.catalogueVariantId | append: '"'
if ars_vto_parts != ""
assign ars_vto_parts = ars_vto_parts | append: ',"'
else
assign ars_vto_parts = '"'
endif
assign ars_vto_parts = ars_vto_parts | append: entry
endif
endfor
assign ars_3d_parts = ""
for variant in product.variants
if variant.metafields.arshades.catalogueVariantId != blank and variant.metafields.arshades.catalogueProductId != blank
assign ars_has_3d = true
assign entry3d = variant.id | append: '":{"variantId":"' | append: variant.metafields.arshades.catalogueVariantId | append: '","productId":"' | append: variant.metafields.arshades.catalogueProductId | append: '"}'
if ars_3d_parts != ""
assign ars_3d_parts = ars_3d_parts | append: ',"'
else
assign ars_3d_parts = '"'
endif
assign ars_3d_parts = ars_3d_parts | append: entry3d
endif
endfor
-%}
Step 2: Render Buttons and Styles
The buttons are rendered only if at least one feature is available. Each button has an additional guard that checks the required global metafields (e.g. catalogueId + webDomain for VTO, viewerId + studioDomain for 3D).
The CSS provides full-width buttons on mobile and auto-width centred buttons on desktop (breakpoint: 769px). You can customise the .vto-button class to match your theme.
{%- if ars_has_vto or ars_has_3d -%}
<style>
.arshades-buttons-row .vto-button {
width: 100%;
}
@media (min-width: 769px) {
.arshades-buttons-row {
justify-content: center !important;
}
.arshades-buttons-row .vto-button {
width: auto !important;
min-width: 0 !important;
max-width: none !important;
flex: 0 0 auto !important;
padding-left: 32px !important;
padding-right: 32px !important;
}
}
</style>
<div class="arshades-buttons-row"
style="display:flex; gap:10px; padding:12px 5px; justify-content:center;">
{%- if ars_has_vto and ars_catalogue_id != blank and ars_web_domain != blank -%}
<button type="button" id="arshades-btn-vto" class="vto-button">
{{- 'virtual-try-on.svg' | inline_asset_content -}}
{{- 'products.vto.button.label' | t | default: 'Virtual Try-On' -}}
</button>
{%- endif -%}
{%- if ars_has_3d and ars_catalogue_id != blank and ars_viewer_id != blank and ars_studio_domain != blank -%}
<button type="button" id="arshades-btn-3d" class="vto-button">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
<path d="M2 17l10 5 10-5"/>
<path d="M2 12l10 5 10-5"/>
</svg>
View in 3D
</button>
{%- endif -%}
</div>
Note: The VTO button uses
'virtual-try-on.svg' | inline_asset_contentand'products.vto.button.label' | t. If these assets/translations are not available in your theme, replace with an inline SVG icon and a hardcoded label string.
Step 3: Embed Data as JSON
The variant maps built in Step 1 are output as <script type="application/json"> blocks. This keeps the data accessible to JavaScript without executing it directly, and avoids issues with Liquid escaping inside inline scripts.
VTO data includes the catalogue ID, the base URL for the VTO iframe, the store language, and the variant-to-catalogueVariantId map.
3D data includes the viewer ID, the studio base URL, and a variant map where each entry has both variantId and productId.
{%- if ars_has_vto -%}
<script type="application/json" id="arshades-vto-data">
{
"catalogueId": {{ ars_catalogue_id | json }},
"shopifyProductId": {{ product.id | json }},
"baseUrl": {{ ars_web_domain | json }},
"language": {{ ars_lang | json }},
"currentVariantId": {{ ars_current_variant.id | json }},
"variants": { {{- ars_vto_parts -}} }
}
</script>
{%- endif -%}
{%- if ars_has_3d -%}
<script type="application/json" id="arshades-3d-data">
{
"viewerId": {{ ars_viewer_id | json }},
"catalogueId": {{ ars_catalogue_id | json }},
"shopifyProductId": {{ product.id | json }},
"baseUrl": {{ ars_studio_domain | json }},
"currentVariantId": {{ ars_current_variant.id | json }},
"variants": { {{- ars_3d_parts -}} }
}
</script>
{%- endif -%}
Step 4: JavaScript — Utilities
The script block wraps everything in an IIFE to avoid polluting the global scope. It starts with two utility sections:
Shopify Analytics Bridge
safePublish() forwards events to Shopify.analytics.publish() so the ARShades Pixel extension can track VTO/3D usage. It fails silently if the Shopify analytics API is not available.
Variant Detection
getActiveVariantId() determines which variant the customer has currently selected. It checks, in order:
- The product form input (
input[name="id"]orselect[name="id"]) — works with most themes. - The
?variant=URL query parameter — fallback for themes that update the URL on selection. - The initially selected variant rendered by Liquid — final fallback.
<script>
(function(){
'use strict';
function safePublish(eventName, payload) {
try {
if (window.Shopify && window.Shopify.analytics &&
typeof window.Shopify.analytics.publish === 'function') {
window.Shopify.analytics.publish(eventName, payload);
return true;
}
} catch (e) { console.warn('[ARShades] publish error', e); }
return false;
}
function getActiveVariantId(fb){
var i = document.querySelector('input[name="id"], select[name="id"]');
if(i && i.value) return String(i.value);
var p = new URLSearchParams(window.location.search);
var v = p.get('variant');
if(v) return String(v);
return String(fb || '');
}
Step 5: JavaScript — Modal System
The modal system creates a full-viewport overlay with an iframe, a loading spinner, and a close button. Key features:
- Lazy creation:
getOrCreateModal()creates the DOM elements only on first use. - Loading state: A spinner is shown until the iframe finishes loading (
iframe.onload). - Close triggers: The X button, clicking the dark overlay, or pressing Escape.
- Scroll lock:
document.body.style.overflow = 'hidden'prevents background scrolling.
The same modal system is reused for both VTO and 3D — each gets its own modalId so they don't conflict.
function getOrCreateModal(modalId, loadingText) {
var existing = document.getElementById(modalId);
if (existing) return existing;
var overlay = document.createElement('div');
overlay.id = modalId;
overlay.setAttribute('style',
'display:none;position:fixed;top:0;left:0;width:100%;height:100%;' +
'z-index:2147483647;background:transparent;' +
'align-items:center;justify-content:center;'
);
overlay.innerHTML =
'<div style="position:absolute;inset:0;background:rgba(0,0,0,0.8);' +
'cursor:pointer;z-index:1;" data-close></div>' +
'<div style="position:relative;z-index:2;width:90vw;height:90vh;max-width:1200px;' +
'background:#fff;border-radius:12px;overflow:hidden;' +
'box-shadow:0 25px 80px rgba(0,0,0,0.5);">' +
'<button data-close style="position:absolute;top:12px;right:16px;z-index:10;' +
'background:rgba(255,255,255,0.95);border:none;width:44px;height:44px;' +
'border-radius:50%;font-size:22px;cursor:pointer;color:#333;line-height:1;' +
'display:flex;align-items:center;justify-content:center;' +
'box-shadow:0 2px 12px rgba(0,0,0,0.2);">×</button>' +
'<div class="ars-loader" style="position:absolute;inset:0;display:flex;' +
'flex-direction:column;align-items:center;justify-content:center;gap:16px;' +
'background:#fff;z-index:5;font-size:14px;color:#666;">' +
'<div style="width:40px;height:40px;border:3px solid rgba(0,0,0,0.1);' +
'border-top-color:#1a1a2e;border-radius:50%;' +
'animation:ars-spin 0.8s linear infinite;"></div>' +
'<span>' + (loadingText || 'Loading...') + '</span>' +
'</div>' +
'<iframe src="" frameborder="0" ' +
'allow="camera *; microphone; gyroscope; accelerometer; xr-spatial-tracking" ' +
'allowfullscreen style="width:100%;height:100%;border:none;display:block;">' +
'</iframe>' +
'</div>';
if (!document.getElementById('ars-spin-style')) {
var st = document.createElement('style');
st.id = 'ars-spin-style';
st.textContent = '@keyframes ars-spin{to{transform:rotate(360deg)}}';
document.head.appendChild(st);
}
document.body.appendChild(overlay);
overlay.querySelectorAll('[data-close]').forEach(function(el){
el.addEventListener('click', function(){ closeModal(modalId); });
});
return overlay;
}
function openModal(modalId, url, loadingText) {
var m = getOrCreateModal(modalId, loadingText);
var iframe = m.querySelector('iframe');
var loader = m.querySelector('.ars-loader');
if (loader) loader.style.display = 'flex';
iframe.src = url;
iframe.onload = function(){ if(loader) loader.style.display = 'none'; };
m.style.display = 'flex';
document.body.style.overflow = 'hidden';
}
function closeModal(modalId) {
var m = document.getElementById(modalId);
if (!m) return;
m.style.display = 'none';
m.querySelector('iframe').src = '';
var loader = m.querySelector('.ars-loader');
if (loader) loader.style.display = 'flex';
document.body.style.overflow = '';
}
document.addEventListener('keydown', function(e){
if(e.key === 'Escape'){
closeModal('arshades-modal-vto');
closeModal('arshades-modal-3d');
}
});
You can customise the modal appearance by changing these inline values:
| Parameter | Default | What it controls |
|---|---|---|
width:90vw;height:90vh | 90% viewport | Modal size |
max-width:1200px | 1200px | Maximum width on large screens |
border-radius:12px | 12px | Corner rounding |
background:rgba(0,0,0,0.8) | 80% opacity | Dark overlay behind the modal |
Step 6: JavaScript — VTO Button Handler
When the VTO button is clicked, the handler:
- Reads the JSON data from
#arshades-vto-data. - Detects the active Shopify variant ID.
- Looks up the corresponding
catalogueVariantIdin the map (falls back to the first available if the current variant is not mapped). - Builds the VTO iframe URL with the required query parameters.
- Publishes analytics events for the ARShades Pixel.
- Opens the modal.
window.addEventListener('message', function(event) {
if (event.data && event.data.type === 'ars_vto_add_to_cart') {
safePublish('arshades:vto_add_to_cart_clicked', {
shopifyVariantId: event.data.variantId ? String(event.data.variantId) : null,
shopifyProductId: event.data.productId ? String(event.data.productId) : null,
source: 'vto_iframe_message'
});
safePublish('ars_vto_add_to_cart_clicked', {
variantId: event.data.variantId ? String(event.data.variantId) : null,
productId: event.data.productId ? String(event.data.productId) : null
});
}
});
var vB = document.getElementById('arshades-btn-vto');
var vD = document.getElementById('arshades-vto-data');
if(vB && vD){
var vto = JSON.parse(vD.textContent);
vB.addEventListener('click', function(){
var sid = getActiveVariantId(vto.currentVariantId);
var cid = vto.variants[sid];
if(!cid){
var k = Object.keys(vto.variants);
if(k.length) cid = vto.variants[k[0]];
}
if(!cid){ alert('No VTO variant available'); return; }
var url = vto.baseUrl
+ '?c=' + encodeURIComponent(vto.catalogueId)
+ '&m=' + encodeURIComponent(cid)
+ '&layout=v1'
+ '&lang=' + encodeURIComponent(vto.language || 'en')
+ '&sin=1';
var payload = {
shopifyVariantId: sid,
shopifyProductId: vto.shopifyProductId ? String(vto.shopifyProductId) : null,
catalogueId: vto.catalogueId,
catalogueVariantId: cid
};
safePublish('arshades:vto_started', payload);
safePublish('ars_vto_started', {
variantId: sid,
productId: vto.shopifyProductId ? String(vto.shopifyProductId) : null,
catalogueId: vto.catalogueId
});
openModal('arshades-modal-vto', url, 'Loading...');
});
}
The VTO URL is constructed as:
| Parameter | Value | Description |
|---|---|---|
c | Catalogue ID | Identifies your ARShades catalogue |
m | Catalogue Variant ID | The specific model to try on |
layout | v1 | VTO interface version |
lang | Store locale (2-letter) | UI language |
sin | 1 | Single-SKU mode (opens directly on the selected variant) |
The message event listener handles add-to-cart actions triggered from within the VTO iframe and forwards them to Shopify analytics.
Step 7: JavaScript — 3D Button Handler
The 3D handler follows the same pattern as VTO but builds a different URL using the Studio domain and viewer ID.
The 3D viewer URL format is:
{studioDomain}/Arshades3d/{viewerId}/glasses/{catalogueProductId}/variant/{catalogueVariantId}
var tB = document.getElementById('arshades-btn-3d');
var tD = document.getElementById('arshades-3d-data');
if(tB && tD){
var td = JSON.parse(tD.textContent);
tB.addEventListener('click', function(){
var sid = getActiveVariantId(td.currentVariantId);
var obj = td.variants[sid];
if(!obj){
var k = Object.keys(td.variants);
if(k.length) obj = td.variants[k[0]];
}
if(!obj){ alert('No 3D variant available'); return; }
var url = td.baseUrl + '/Arshades3d/' + td.viewerId
+ '/glasses/' + obj.productId
+ '/variant/' + obj.variantId;
safePublish('arshades:3d_opened', {
shopifyVariantId: sid,
shopifyProductId: td.shopifyProductId ? String(td.shopifyProductId) : null,
catalogueId: td.catalogueId,
viewerId: td.viewerId,
catalogueVariantId: obj.variantId,
catalogueProductId: obj.productId
});
openModal('arshades-modal-3d', url, 'Loading 3D...');
});
}
})();
</script>
{%- endif -%}
{%- comment -%} ===== END ARShades Buttons ===== {%- endcomment -%}
ARPDMeter Manual Integration
If your theme does not support App Blocks for the ARPDMeter component, you can integrate the PD Meter button manually. This requires the ARShades bundle to be loaded (either via the vto-embed-loader App Embed or via the manual loader script from the VTO integration above).
Prerequisites
- An active ARPDMeter license (
shop.metafields.arshades.has_arpdmust be"true"). - The ARShades bundle must be loaded on the page (provides
window.ARShadesVTO.openPDTool). - The license ID metafield must be present (
shop.metafields.arshades.licenseId).
Step 1: Check License and Read Metafields
The first block checks if the ARPDMeter module is enabled for this store by reading the has_arpd and licenseId metafields. The button is only rendered when both conditions are met.
{%- comment -%} ===== ARShades PD Meter Button ===== {%- endcomment -%}
{%- liquid
assign has_arpd = shop.metafields.arshades.has_arpd
assign licence_id = shop.metafields.arshades.licenseId
-%}
{%- if has_arpd == "true" and licence_id != blank -%}
If the license is not active, you can optionally show a placeholder in the Theme Editor (design mode only):
{%- else -%}
{%- if request.design_mode -%}
<div style="padding: 16px; border: 1px dashed #ccc; border-radius: 8px;
text-align: center; color: #888; font-size: 14px;">
<p style="margin: 0 0 8px;">ARShades PD Meter</p>
<p style="margin: 0; font-size: 12px;">
This block requires an active ARPDMeter license.
Activate it from the ARShades app settings.
</p>
</div>
{%- endif -%}
{%- endif -%}
Step 2: Button HTML and Icon
The button includes an optional SVG crosshair icon and a customisable text label. You can replace the icon with your own or remove it entirely.
<div class="arshades-pdmeter-container" data-arshades-pdmeter>
<button type="button"
id="arshades-pdmeter-button"
class="arshades-pdmeter-button"
aria-label="Measure PD">
<!-- Default crosshair icon (replace or remove as needed) -->
<svg class="arshades-pdmeter-icon" width="24" height="24"
viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
aria-hidden="true">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.5"/>
<circle cx="12" cy="12" r="4" stroke="currentColor" stroke-width="1.5"/>
<line x1="12" y1="2" x2="12" y2="6" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round"/>
<line x1="12" y1="18" x2="12" y2="22" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round"/>
<line x1="2" y1="12" x2="6" y2="12" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round"/>
<line x1="18" y1="12" x2="22" y2="12" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round"/>
</svg>
<span>Measure PD</span>
</button>
</div>
Step 3: Button Styling
The CSS uses CSS custom properties so you can easily override colours, radius, and sizing from one place. Adjust the values to match your theme.
<style>
.arshades-pdmeter-container {
--arshades-pd-primary: #000000;
--arshades-pd-primary-hover: #333333;
--arshades-pd-text: #ffffff;
--arshades-pd-btn-radius: 8px;
--arshades-pd-transition: 0.2s ease;
}
.arshades-pdmeter-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
padding: 14px 24px;
background-color: var(--arshades-pd-primary);
color: var(--arshades-pd-text);
border: none;
border-radius: var(--arshades-pd-btn-radius);
font-family: inherit;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background-color var(--arshades-pd-transition),
transform var(--arshades-pd-transition),
box-shadow var(--arshades-pd-transition);
text-decoration: none;
line-height: 1.4;
}
.arshades-pdmeter-button:hover {
background-color: var(--arshades-pd-primary-hover);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.arshades-pdmeter-button:active {
transform: translateY(0);
}
.arshades-pdmeter-button:focus-visible {
outline: 2px solid var(--arshades-pd-primary);
outline-offset: 2px;
}
.arshades-pdmeter-icon {
flex-shrink: 0;
width: 24px;
height: 24px;
}
</style>
| CSS Variable | Controls | Default |
|---|---|---|
--arshades-pd-primary | Button background colour | #000000 |
--arshades-pd-primary-hover | Hover state colour | #333333 |
--arshades-pd-text | Button text colour | #ffffff |
--arshades-pd-btn-radius | Corner radius | 8px |
Step 4: JavaScript — Open PD Meter
When the button is clicked, it calls ARShadesVTO.openPDTool() with the license token, interface colours, modal dimensions, and language. If the bundle is still loading, the button shows a "Loading..." state and retries automatically when ready.
The script also listens for two events from the PD Meter:
arshades:pdmeter:result— fired when measurement is completed successfully.arshades:pdmeter:error— fired on errors (e.g. missing license), which disables the button.
<script>
(function() {
'use strict';
var button = document.getElementById('arshades-pdmeter-button');
if (!button) return;
button.addEventListener('click', function() {
if (window.ARShadesVTO &&
typeof window.ARShadesVTO.openPDTool === 'function') {
window.ARShadesVTO.openPDTool({
licenseToken: {{ licence_id | json }},
arpdPrimaryColor: '#42B1AC',
arpdSecondaryColor: '#42B1AC',
arpdModalWidth: 100,
arpdModalHeight: 100,
language: '{{ request.locale.iso_code | slice: 0, 2 }}'
});
} else if (window.ARShadesLoader &&
!window.ARShadesLoader.isLoaded()) {
button.textContent = 'Loading...';
window.ARShadesLoader.onReady(function() {
button.textContent =
button.getAttribute('aria-label') || 'Measure PD';
button.click();
});
} else {
console.error(
'[ARShades PD Meter] Bundle not loaded. ' +
'Ensure vto-embed-loader is enabled in theme settings.'
);
}
});
window.addEventListener('arshades:pdmeter:result', function(e) {
console.log('[ARShades PD Meter] Result:', e.detail);
});
window.addEventListener('arshades:pdmeter:error', function(e) {
if (e.detail && e.detail.code === 'LICENSE_MISSING') {
button.disabled = true;
button.style.opacity = '0.5';
button.title = 'PD Meter license not active';
}
});
})();
</script>
openPDTool Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
licenseToken | String | — | Your ARShades license ID (required) |
arpdPrimaryColor | String (HEX) | #42B1AC | Primary accent colour in the measurement interface |
arpdSecondaryColor | String (HEX) | #42B1AC | Secondary colour in the measurement interface |
arpdModalWidth | Number | 100 | Modal width in % |
arpdModalHeight | Number | 100 | Modal height in % |
language | String | en | Two-letter language code (supported: en, it, de, es, fr) |
PD Meter Events
| Event | Payload | Description |
|---|---|---|
arshades:pdmeter:result | e.detail — measurement data | Fired when the user completes a measurement |
arshades:pdmeter:error | e.detail.code — error code | Fired on errors (e.g. LICENSE_MISSING) |
Adaptation Notes
When integrating into a specific theme, you may need to adjust:
- SVG icon: The VTO button references
'virtual-try-on.svg' | inline_asset_content. If not available in your theme, replace with an inline SVG or remove. - Translation filter:
'products.vto.button.label' | tuses your theme's locale files. Replace with a hardcoded string if the key doesn't exist. - Variant detection: The code relies on
input[name="id"]/select[name="id"]for variant detection. If your theme uses a different mechanism (Web Components, custom events), adjustgetActiveVariantId(). - CSS conflicts: If your theme's button styles override
.vto-button, use more specific selectors or!important.
Technical Appendix: Metafields
The ARShades plugin writes these metafields automatically when your license is connected and products are synchronised.
Shop Metafields (Global)
| Metafield | Description |
|---|---|
shop.metafields.arshades.catalogueId | Your ARShades Catalogue ID |
shop.metafields.arshades.webDomain | Base URL for VTO (e.g. https://webvto.it) |
shop.metafields.arshades.viewerId | The 3D Viewer instance ID |
shop.metafields.arshades.studioDomain | Base URL for 3D Viewer (e.g. https://studio.arshades.it) |
shop.metafields.arshades.has_arpd | "true" if the ARPDMeter module is active |
shop.metafields.arshades.licenseId | License ID used to authenticate ARPDMeter calls |
Variant Metafields (Per-SKU)
| Metafield | Description |
|---|---|
variant.metafields.arshades.catalogueVariantId | Unique variant ID for VTO and 3D |
variant.metafields.arshades.catalogueProductId | Product ID for the 3D Viewer |