Live playground

Change it here, paste it into your product page.

Every control below edits the real element on the left. The snippet updates as you go, and the event log shows exactly what your page would receive.

Checking this origin…

The element

Attributes

Which frame the widget starts on. Your own catalogue replaces these.
Starts the camera without a click. A bad default on a product page — it fetches ~5 MB and prompts for the camera before the shopper asked.
The try-on tracks the face with MediaPipe, downloaded on the shopper's first click (~5 MB, never on page load). This is the folder holding its WebAssembly runtime. Empty = Google's public CDN.
The trained face-landmark model, face_landmarker.task. Empty = Google's public CDN. For a live store, host both files on your own domain so the try-on never depends on a third party: copy node_modules/@mediapipe/tasks-vision/wasm and download the .task file into your static folder, then fill in both fields — one without the other quietly mixes your runtime with a CDN model.

Events your page receives

  • Nothing yet — press start.

Studio shot

Optional second element, <eyewear-studio>: the shopper uploads a photo and gets it back as a clean studio shot (neutral backdrop, even light, frames untouched). It never sees the camera or the try-on — only the file the shopper chose leaves the page. The endpoint is a URL on your server that holds the image-service key and forwards the request; drop tools/studio-proxy.mjs (one function, no dependencies) into your backend. Here it points at this playground's own built-in copy. Empty = the element is not rendered at all.

Dropping it into a real store

The element is plain HTML with a script tag, so it goes wherever your theme lets you put markup. Three places that usually means.

<script type="module" src="https://your.cdn/eyewear-vto.js"></script>
<eyewear-tryon sku="ACME-52"></eyewear-tryon>

That is the whole integration. The element renders a start button and stays inert until it is pressed, so it costs the page essentially nothing until a shopper uses it.

<!-- sections/product-tryon.liquid -->
<script type="module" src="{{ 'eyewear-vto.js' | asset_url }}"></script>
<eyewear-tryon sku="{{ product.selected_or_first_available_variant.sku }}">
</eyewear-tryon>

{% schema %}
{ "name": "Virtual try-on", "target": "section" }
{% endschema %}

Upload the bundle as a theme asset rather than hotlinking a CDN — Shopify serves assets from its own domain, so you avoid a third-party request on every product page. Re-render the element's sku when the variant picker changes; the widget swaps frames without restarting the camera.

// functions.php
add_action( 'woocommerce_single_product_summary', function () {
  global $product;
  wp_enqueue_script_module(
    'eyewear-vto',
    get_stylesheet_directory_uri() . '/js/eyewear-vto.js'
  );
  printf(
    '<eyewear-tryon sku="%s"></eyewear-tryon>',
    esc_attr( $product->get_sku() )
  );
}, 35 );

Priority 35 puts it below the add-to-cart button and above the meta block. wp_enqueue_script_module needs WordPress 6.5+; before that, print the <script type="module"> tag yourself — wp_enqueue_script cannot emit one.