Customize the upload and result modal
Available in the Liquid extension. Existing themes retain their default experience unless theme code uses these hooks.
For versioned processing state, result image access and progress presentation, see the separate theme integration API preview. Those additions require a compatible extension release; the hooks below remain supported independently.
No merchant setting or registration API is required. Install a theme script before the shopper first opens VTO. Without listeners, the existing experience is unchanged. These hooks apply to the Liquid extension, not the Hydrogen SDK.
Events
Section titled “Events”Both events originate from the owning .aabhas-vto-block and bubble to
document. Their detail includes { version: 1, modal: HTMLElement }.
The result event additionally includes result: { productId, variantId, source }.
IDs are strings or null; source is generation or history. Missing history
identity is null, never inferred from the currently open product. This is a
UI notification, not a trusted backend assertion.
aabhas:vto:modal-ready: synchronous, once after generated modal labels and consent copy are configured, before controls are queried and bound. Rearrange existing nodes here; do not await asynchronous work before moving controls.aabhas:vto:result-visible: the result view has been opened, including history results. It does not certify that the image has finished downloading. Use the image’sloadevent for that. This event is not an analytics submission API.
For extensions implementing the lifecycle preview,
the result state snapshot is published before result-visible. A native close
from either notification is preserved. If an earlier state/focus listener closes
or removes the view, result-visible is skipped. Do not assume these preview
ordering guarantees exist in an older extension release.
Theme example
Section titled “Theme example”Add this JavaScript once in a theme asset. It decorates the existing modal; there is no second modal and no copied generation implementation.
document.addEventListener('aabhas:vto:modal-ready', (event) => { if (event.detail.version !== 1) return; const {modal} = event.detail; modal.classList.add('brand-vto'); // Add brand-owned headers/panels or move existing controls within this modal.});
document.addEventListener('aabhas:vto:result-visible', (event) => { const {modal, result} = event.detail; // Resolve comparison media using result.productId and result.variantId. // If either is unavailable in your catalog, hide the comparison panel. // Do not issue a second generation or emit duplicate success analytics. modal.classList.add('brand-vto-result');});Scope theme CSS to .brand-vto. In default modal mode, the runtime moves the modal to document.body
when opening, so selectors under the product section do not apply. The preview
inline presentation
keeps the same root inside its owning block. Choose it before mounting instead
of using an observer to move the root. Default
runtime CSS loads on demand; use sufficiently specific scoped selectors rather
than relying on stylesheet order. Reuse fonts already loaded by the theme.
Supported customization boundary
Section titled “Supported customization boundary”Keep the existing .aabhas-vto-modal root, .aabhas-vto-dialog, close control,
upload/file-input controls, consent controls, crop subtree, and result subtree.
Move nodes rather than cloning/replacing them. Preserve their classes, labels,
ARIA attributes, disabled states and hidden attributes. Keep controls inside
the modal; do not reparent its root into another dialog. The runtime owns
.is-result, crop state, result image URL, visibility and event handlers.
Keep .aabhas-vto-dialog-content and .aabhas-vto-result-viewer as separate
siblings inside .aabhas-vto-dialog. Never place the result viewer or close
button inside dialog-content: that entire container is hidden in result mode.
Keep the crop subtree intact; its positioning depends on its existing parents.
Place comparison content inside the result viewer alongside the result image.
The companion example below shows a product comparison layout.
Style or wrap the existing upload and result sections to share one branded shell. A comparison panel may contain a product image from the theme. Do not copy customer photos or signed result URLs to analytics, logs or persistent theme storage. The modal is already accessible to same-page theme scripts; these events are not a security isolation boundary.
This is not arbitrary replacement of the execution engine. Login, consent, validation, reuse, history and generation remain owned by Aabhas. The default modal CSS and runtime are still downloaded. No extra network request is added by these events; custom theme assets have their own performance cost.
Before publishing a theme, test mobile crop, upload/reuse, consent, required login, errors, history, close/reopen, variant changes and multiple sections. Keep a visible close button, keyboard focus and unobstructed consent. Removing required controls or overriding runtime state is unsupported.
Comparison script
Section titled “Comparison script”// Load once before VTO opens. Supply product images through theme Liquid.(() => { const images = JSON.parse(document.querySelector('[data-vto-comparison-images]')?.textContent || '{}'); document.addEventListener('aabhas:vto:modal-ready', ({detail}) => { if (detail.version !== 1) return; const {modal} = detail; modal.classList.add('brand-vto'); const original = document.createElement('img'); original.className = 'brand-vto-original'; original.alt = 'Original product'; original.hidden = true; modal.querySelector('.aabhas-vto-result-viewer').prepend(original); }); document.addEventListener('aabhas:vto:result-visible', ({detail}) => { if (detail.version !== 1) return; const {modal, result} = detail; const original = modal.querySelector('.brand-vto-original'); if (!original) return; const url = result.productId && result.variantId && images[`${result.productId}:${result.variantId}`]; original.hidden = !url; modal.classList.toggle('has-comparison', Boolean(url)); if (url) original.src = url; else original.removeAttribute('src'); });})();Comparison styles
Section titled “Comparison styles”.brand-vto .aabhas-vto-dialog { background: #111; color: #fff; }.brand-vto.is-result.has-comparison .aabhas-vto-result-viewer { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; padding: 48px 12px 64px; box-sizing: border-box; background: #111;}.brand-vto.is-result.has-comparison .aabhas-vto-result-image,.brand-vto.is-result.has-comparison .brand-vto-original { width: 100% !important; height: 100% !important; min-height: 0; min-width: 0; max-width: 100% !important; max-height: 100% !important; object-fit: contain !important;}.brand-vto .brand-vto-original[hidden] { display: none !important; }@media (max-width: 749px) { .brand-vto.is-result.has-comparison .aabhas-vto-result-viewer { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); }}Add this Liquid before the script to map the current product’s variants:
<script type="application/json" data-vto-comparison-images>{{% for variant in product.variants %} {% capture key %}{{ product.id }}:{{ variant.id }}{% endcapture %} {% assign image = variant.featured_image | default: product.featured_image %} {{ key | json }}: {{ image | image_url: width: 1200 | json }}{% unless forloop.last %},{% endunless %}{% endfor %}}</script>Historical results not in this map display without a comparison. Never substitute the current variant for an unknown history variant. Keep VTO controls and consent intact. The example changes presentation only, not API behavior or analytics.