What to do with each event
The configurator tells you what the shopper is doing through browser events. Nothing to switch on: they are always emitted, and if nobody listens nothing happens. This page goes event by event with concrete ideas; the technical contract (shared fields, versioning, what they never carry) lives in Theme integration.
How to listen
Section titled “How to listen”They are dispatched on <optical-form-drawer> and bubble, so listening on document is enough. All of them carry a detail with the same context: sessionId (stitches a whole visit together), productId, variantId, stepIndex, stepId, stepType and totalSteps.
document.addEventListener('opticalform:open', (e) => { console.log('configurator opened on product', e.detail.productId);});Two limits worth knowing before you build: the events never carry the shopper’s answers (no prescription, no file, no texts) and cannot be cancelled. They say where the shopper is, not what they answered, and they do not let you intervene.
The nine events
Section titled “The nine events”opticalform:open — the configurator opens
Section titled “opticalform:open — the configurator opens”The first moment you know this person wants to build glasses, not just look at a frame.
- Load your chat only now. If your chat loads lazily for privacy reasons, this is the right trigger: someone who looks without touching leaves no third-party cookies.
- Tag the session in your chat or analytics: “is configuring glasses” is worth more than “viewed a product”.
- Start a clock: the time from here to
:addtocart:successis your real configuration time.
document.addEventListener('opticalform:open', () => { if (!window.myChatLoaded) loadMyChat(); // your function, your provider});opticalform:step:rendered — the shopper reaches a step
Section titled “opticalform:step:rendered — the shopper reaches a step”Also carries isFirst and isLast. Emitted when the step changes, not on every re-render.
- Funnel by step in your analytics: you will know where people stop, which is the question that decides what to fix.
- A contextual chat message when they reach the prescription (
stepTypegraduation,reading_rxorcontact_lens): “Got your prescription handy? If not, upload a photo.” Once per session, not on every step. - Prepare the finish when
isLastistrue: for example, mention free shipping.
document.addEventListener('opticalform:step:rendered', (e) => { dataLayer.push({ event: 'lens_step', step: e.detail.stepType, index: e.detail.stepIndex });});opticalform:validation:error — a step will not let them move on
Section titled “opticalform:validation:error — a step will not let them move on”It does not carry the failing value, only the step.
- Count where people stumble. If one step concentrates the errors, the step is the problem: its help text, its example, whether it should be required.
- Offer help after a few tries: on the second or third error on the same step, the chat can open with a direct question.
opticalform:addtocart:start — the add begins
Section titled “opticalform:addtocart:start — the add begins”Between this event and the next one the configurator is talking to the Shopify cart.
- Hide what gets in the way while adding, such as a chat launcher floating over the button.
- Measure the latency to
:addtocart:success: if it grows, something changed in the theme or in a cart app.
opticalform:addtocart:success — the cart accepted it
Section titled “opticalform:addtocart:success — the cart accepted it”Carries lineCount, totalPrice (in cents) and currency. This is your conversion event.
- Send the conversion to GA4, Meta or TikTok with its value. Before this moment the price deliberately does not travel; after it, it is already in
/cart.js. - Recommend the extra they skipped: a case, a second pair, a treatment.
- Explain the cart to first-timers: the frame and every lens or treatment arrive as separate lines with their own price. That is what your lab receives.
document.addEventListener('opticalform:addtocart:success', (e) => { gtag('event', 'add_to_cart', { value: e.detail.totalPrice / 100, currency: e.detail.currency, items: [{ item_id: String(e.detail.productId), quantity: e.detail.lineCount }], });});opticalform:addtocart:error — it failed
Section titled “opticalform:addtocart:error — it failed”Carries kind — the cause — and line — which line failed. kind is one of sold_out (the frame is sold out), addon_unavailable (a treatment, colour or accessory cannot be sold), limit (the cart already holds all the stock), unavailable (something no longer exists), throttled (Shopify asked to wait) or unknown; line is frame, addon or unknown. It also fires when the drawer refuses to send without calling Shopify, because it already knew the frame or an add-on was sold out.
- Alert support with
sessionId,productId,kindandline: you get there before the shopper writes. - Retry only when
throttled; the others are not fixed by repeating. - Watch
addon_unavailable: each one is a lens, colour or accessory variant that couldn’t be sold while people were choosing it — the editor shows it under Check prices.sold_outis the frame: the product-page button should already read “Sold out”.
opticalform:close — it closes
Section titled “opticalform:close — it closes”Carries completed: true if they finished, false if they left. That is the whole difference when recovering an abandonment.
- Abandonment with context: with
completedatfalseyou know the step (stepType,stepIndex) and the product. A chat message, a “we saved your configuration” note or an email form are worth more here than any exit popup. - Leave finishers alone: with
completedattrue, the only thing to do is let them go to the cart.
document.addEventListener('opticalform:close', (e) => { if (!e.detail.completed) myChat.message('Any questions? Write to us here.');});opticalform:resume — the shopper resumes a half-finished configuration
Section titled “opticalform:resume — the shopper resumes a half-finished configuration”If they closed the configurator before finishing and come back to the same product within 7 days, the app offers “Continue where you left off”. This event fires when they press it. It carries restoredSteps: how many steps got their answer back. Like every event, it does not say what they answered. And it only fires if the merchant has turned on “Remember a half-finished configuration” under Design › Navigation (it is off by default).
- Measure whether it helps: how many
resumeagainst how manyclosewithoutcompletedtells you whether it is worth keeping on. The Analytics tab already counts it (“N resumed a half-finished configuration”). - Change the chat’s tone: someone resuming has already decided half of it; “need a hand with the prescription?” fits better than “shall we start?”.
document.addEventListener('opticalform:resume', (e) => { myAnalytics.event('configurator_resumed', { steps: e.detail.restoredSteps });});optical-form:cart:orphan — a child line is missing in the cart
Section titled “optical-form:cart:orphan — a child line is missing in the cart”This one is not emitted by the configurator but by the cart bridge, on document, when it detects a frame without the lenses or treatments it left with: the customer removed them from the theme’s cart. Carries groupId, key, expected and actual. The app paints a translated notice under the row and removes nothing and blocks no checkout.
- You decide: remove the frame, warn through chat, or let it through if in your store a frame can be bought on its own. It is a merchant decision, not a bug.
- Measure it: how many remove the lenses tells you whether the add-on price surprises people in the cart.
Three complete recipes
Section titled “Three complete recipes”Funnel in Google Analytics 4. Listen to :open, :step:rendered, :validation:error, :addtocart:success and :close, and send each as an event with stepType and stepIndex. In GA4, a funnel report by stepIndex shows the drop step by step; sessionId as a custom dimension stitches the whole visit together.
A chat that accompanies. Load your provider on :open, keep in sessionStorage which messages you have already shown so you do not repeat them, and send at most one per milestone: reaching the prescription, adding to the cart, closing without finishing. If your chat lives at the bottom right, make room for it with the Space kept free at the right of the footer setting (Design › Navigation) so it does not cover the continue button: see Design & cart.
Abandonment recovery. On :close with completed at false, store productId and stepIndex; if you have the customer’s email (they are logged in, or your popup asks for it), your email tool can send a “pick up where you left off” with the product link. The app does not save half-finished configurations: what you recover is the intent, not the answers.
What you cannot do with them
Section titled “What you cannot do with them”- Cancel a step, an add or a close:
preventDefault()does nothing, on purpose. - Read the prescription, the uploaded file or the merchant’s texts: the
detailis a closed list.stepTypedoes travel, and it is inferred health data; treat it as such. - Change what a step validates: that is configured in the form, not in a script.
Versioning and re-render details are in Theme integration.