DEV Community

Cover image for Virtual Eyewear Try-On: 4 Jobs Between Frame Photos and Cart
Team Banuba
Team Banuba

Posted on AI-assisted

Virtual Eyewear Try-On: 4 Jobs Between Frame Photos and Cart

Putting eyewear try-on on a storefront breaks into four jobs: turning photographs of each frame into a try-on asset, deciding which browsers get the entry point, mounting the widget and choosing what opens it, and connecting the catalog check to the cart events. Three are front-end work. The first is a data pipeline, and it sets the launch date.

The ticket usually arrives with a design attached, a button under the add-to-cart block, and no picture of what sits behind it. The code below uses the Banuba virtual try-on widget because that is what we build, so the attribute and method names are ours, and every fact about them is cited to our own documentation. The four jobs hold for any web try-on engine you mount.

Before you start

Four things belong in hand before the first commit:

  • An HTTPS origin for every environment the widget runs in, staging included.
  • A merchant ID issued by Banuba, which the widget uses to load your configuration.
  • Photographs of every frame in the launch collection, shot to the digitization spec rather than pulled from the web catalog.
  • A decision about where the entry point lives on the product detail page, since it drives the gating logic in the second job.

Digitizing the frames

Digitization turns photographs of a physical frame into the asset the widget renders on a face, and in the admin dashboard you do it one frame at a time. Per frame you supply a name, a brand picked from the list or created on the spot, a SKU, an optional product page URL, then three images in the try-on model step: left, body and right. A preview renders while the images upload, so a bad set shows itself before you save, as our glasses dashboard guide describes.

Since widget v1.7.0, released 30 April 2026 according to our widget changelog, that processing runs automatically. The consequence lands on whoever owns the photography. Our docs state that photos failing the requirements give a poor result, and that it cannot be repaired afterwards, because nothing about the processing is manual. Treat the photo spec as an input requirement with the same weight as a schema.

Here is the part that reshapes a project plan. The bulk CSV path is makeup only. In our bulk upload template the Category column always takes the value MAKEUP, and the columns after it are Region, Finish, Color as a 6-digit hex, and Coverage, all parameters of a cosmetic product. Frames therefore go in through the Glasses tab individually, and our docs mark that tab Beta. For a catalog of any size that is a data entry line with a person's name on it, and sizing it comes before anyone commits to a launch date.

Production work asks for more than the three try-on images. The commercial list in our knowledge base adds UPC number, a storefront image link, the product page URL, photos from multiple angles, and a 3D model, with frame size where it is known. Collect all of it in the same pass, since walking the collection twice costs the same hours twice.

The decision point is ownership. Someone has to supply the photographs, either merchandising from existing shoots or a studio from a new one, and that answer moves the timeline more than any code in this article does. Pull one frame record out of your PIM as well and see whether the frame size fields exist there at all.

What goes wrong here is scheduling. A team scopes the work as a front-end sprint, then finds in week one that the launch collection has to be entered by hand and reshot on top, because the existing photography was styled for hero banners at an angle the try-on step cannot use.

Deciding which browsers get the button

The try-on button should render off a runtime check rather than a static support table, because camera access has hard requirements a shopper's browser either satisfies or fails. The first is transport. Webcam access needs an HTTPS/TLS connection, so a staging box on plain HTTP fails at the first call and looks exactly like a broken widget; MDN documents the secure context rule behind getUserMedia, which applies to every camera API on the page, ours included.

The second is the engine. Our documented system requirements specify the last 2 Safari major versions, the last 2 iOS major versions, and the last 5 versions of Chrome, Opera, Edge and Samsung Internet. Read that as the set we test against. A version floor describes a population, while the shopper in front of you is one device with one driver and one browser build.

The answer for an individual shopper comes from feature detection at render time:

function cameraIsAvailable() {
  return Boolean(
    window.isSecureContext &&
    navigator.mediaDevices &&
    typeof navigator.mediaDevices.getUserMedia === 'function'
  );
}

const tryOnButton = document.getElementById('open-vto');
tryOnButton.hidden = !cameraIsAvailable();
Enter fullscreen mode Exit fullscreen mode

window.isSecureContext covers transport, navigator.mediaDevices is absent entirely on browsers without media capture, and tryOnButton is the entry point your PDP template renders. The button now appears only where both conditions hold.

Then comes the branch, and both sides are defensible. Hiding the entry point on a failing check keeps the page honest, at the cost of a shopper who never learns the feature exists. Rendering it with a static model photo behind it keeps the feature discoverable. Eyewear weakens that second option in a way a cosmetic shade does not: a frame on a stock model shows how the style reads, while the question that made the shopper tap was how it sits on their own face.

Skip the gate and the failure is uniform. Every shopper sees the button, and the ones on an old in-app browser or an HTTP staging link get a modal that opens onto nothing.

Mounting the widget and picking the trigger

Mounting is two lines of markup, and the judgment call here is what opens the widget afterwards. The bundle loads from a CDN, and the element goes wherever the experience should appear, both covered in our integration guide:

<head>
  <script type="module" src="https://tintvto.com/widget.js"></script>
</head>
Enter fullscreen mode Exit fullscreen mode

That path carries no version segment, so it resolves to the current release. Confirm a pinned URL with your technical manager before production, because a minor release can change behavior on a page you have not deployed to in months.

On a product detail page you want the widget focused on the frame being viewed, which is what the isolated SKU attributes do:

<tint-vto merchant-id="YOUR_MERCHANT_ID" isolated-sku sku="8028997081552"></tint-vto>
<button id="open-vto" data-sku="8028997081552" hidden>Try these on</button>
Enter fullscreen mode Exit fullscreen mode

merchant-id is the identifier Banuba issues you, isolated-sku switches the widget into single product mode, and sku names the frame on this page. The token and sdk-token attributes are documented as deprecated and slated for removal, so check any snippet inherited from an older sample.

That merchant ID ships in your page source and any visitor can read it in devtools, which is normal for a widget that loads its configuration client side and is also the first question a security reviewer will ask. The answer worth having in writing before launch is how the identifier is scoped: which origins it works from, how that list is configured, and what happens when someone copies it onto their own domain. Put those three questions to your technical manager during evaluation rather than the week before release.

Our integration guide documents launch triggers for opening immediately, opening on the window load event, and opening on a customer action. A product detail page wants the third, so that the camera permission prompt follows an explicit tap and reads as an answer to something the shopper asked for.

The camera itself can be declined, and the fallback is two method calls:

<script type="module">
  const widget = document.querySelector('tint-vto');
  const tryOnButton = document.getElementById('open-vto');

  tryOnButton.addEventListener('click', async () => {
    await widget.open();

    const stream = await widget.useWebcam();
    if (!stream) {
      const modelPhoto = await fetch('/media/model-front.webp').then((r) => r.blob());
      await widget.useImage(modelPhoto);
    }
  });
</script>
Enter fullscreen mode Exit fullscreen mode

useWebcam() resolves with the active stream, or with null when the shopper denies access, and useImage() accepts a Blob or File to render on instead. usePhoto() is a deprecated alias of useImage(). On a frames PDP that fallback stays a courtesy, for the fit reason above.

Two things break here in practice. One is the inherited snippet still passing token, which keeps working until the release that removes it. The other is a widget wired to open on page load, which spends the camera prompt before the shopper has expressed any interest and trains them to dismiss it.

Connecting the catalog and the cart

Two wires finish the integration: a catalog check deciding whether the button renders for this frame, and the events keeping the cart in step with the widget. The check is isCustomerSkuExist(customerSkuIds: string[]), which resolves true when every SKU you pass exists in the try-on catalog and can be called before open(), per our widget API reference. Put it in the PDP render path:

<script type="module">
  const widget = document.querySelector('tint-vto');
  const tryOnButton = document.getElementById('open-vto');
  const currentSku = tryOnButton.dataset.sku;

  const isDigitized = await widget.isCustomerSkuExist([currentSku]);
  tryOnButton.hidden = !isDigitized || !cameraIsAvailable();
</script>
Enter fullscreen mode Exit fullscreen mode

currentSku reads the data-sku attribute your template wrote onto the button, and cameraIsAvailable() is the detection function from the previous job, imported from wherever you keep it. A frame with no try-on asset yet never shows a button.

That gate carries more weight for eyewear than for a cosmetic shade, where a missing asset still leaves a swatch and a product photo to look at.

Cart sync runs on the events the widget emits. Attach the listeners once, wherever you mount the element:

const cart = window.storefrontCart; // your own cart module

widget.addEventListener('addToCart', ({ detail }) => cart.add(detail));
widget.addEventListener('removeFromCart', ({ detail }) => cart.remove(detail));
widget.addEventListener('redirectToCart', () => { window.location.href = '/cart'; });
Enter fullscreen mode Exit fullscreen mode

Our API reference documents detail as an array of Product objects on the first two events, and as null on redirectToCart, which fires when the shopper clicks Go to cart. To drive the widget from your own product data instead, applyProduct() takes fully defined product objects and applyProductByCustomerSku() takes your customer facing SKU identifiers.

For device checks, widget v1.7.1 of 18 May 2026 added try-on links and QR codes generated per product or for a whole catalog, per the same changelog. QA becomes scanning a code: open it on the phones your analytics say people shop from, and walk the flow inside the Instagram and Facebook in-app browsers, with no deploy behind any of it. Rendering and camera behavior diverge across handsets much further than a support matrix suggests, which is what our web AR testing kept running into.

The defects to watch for are quiet ones. Leave any of those events unhandled and the cart drifts, so a shopper adds a frame inside the try-on and finds the header count unchanged. Ship a frame to the PDP with a live button before anyone has processed its photos and you get the ungated version of the check above.

What carries over to another engine

Swap the vendor and the identifiers change while the shapes survive. There is always an asset preparation step with a photography spec attached, and it is the long pole. A capability gate follows it, since camera access on the web is conditional everywhere. Then a mount point plus a trigger decision, where the case for the tap is the permission prompt. Last comes the boundary between the try-on surface and your cart, crossed by events one way and product identifiers the other.

Bottom line

The three front-end jobs are a normal week for someone holding the merchant ID and a PDP template. The variable is the photography, and better code will not compress it, because it is bounded by how fast someone shoots frames to spec and enters them one at a time through a Beta tab. So run the estimate in that order: count the frames in the launch collection, find out who is shooting them and whether the UPC and frame size fields exist in your PIM today, and put the code work after that number.

FAQ

What do you upload for each frame?

A name, a brand, a SKU, an optional product page URL, and three images of the frame: left, body and right. Going to production, collect the commercial set in the same pass, which adds UPC number, a storefront image link, photos from multiple angles, a 3D model, and frame size where you have it.

Can frames be bulk imported from a CSV?

No. The bulk upload template is makeup only: its Category field takes the single value MAKEUP and its parameter columns are Region, Finish, Color and Coverage. Frames go in individually through the Glasses tab of the admin dashboard.

Why does the try-on work locally but not on my staging server?

Almost always because staging is served over plain HTTP. Browsers restrict camera access to secure contexts, and localhost counts as one while http://staging.example.com does not. Put a certificate on staging and the same build works.

Does eyewear try-on need a native app?

No. The widget is web based, loads from a CDN script tag, and runs in the browser on desktop and mobile, so no app store release sits in the critical path.

What happens on an unsupported browser?

The widget opens and the camera call fails, which reads to the shopper as a broken page. Hence the feature detection in the second job: the entry point renders only where a secure context and a media devices API are both present.

Which attributes should a new integration use?

merchant-id, plus isolated-sku and sku on a product detail page. token and sdk-token are deprecated and scheduled for removal, and usePhoto() is a deprecated alias of useImage().

Top comments (0)