DOM and Shadow-DOM Remediation Layer for Yotpo, Rebuy and Recharge Widgets
Corrections applied to markup the theme didn't write: MutationObserver routines that re-level a review widget's headings, demote headings inside a cart drawer, re-anchor a subscription add-to-cart control, and append a stylesheet inside a subscription widget's shadow root — every one re-entrant, so the fix converges instead of looping.
A theme controls the markup it renders, not what an installed app injects after load. On Three Ships the page needed one heading level under the review summary, a single outline through the cart drawer, the subscription add-to-cart in the buy box's slot, and its own type inside a subscription widget's shadow root. Observers in the layout apply each correction, and every one converges: an observer that sees its own change does nothing.
The Problem
A Shopify theme owns the HTML it renders. An installed app's storefront widget renders its own HTML into the page after load, re-renders it on state change, and — where the vendor uses a shadow root — puts it behind a boundary the theme's stylesheet can't cross. The document outline and the CSS cascade stop where the widget starts.
On Three Ships the page needed four things from markup the theme doesn't render: one heading level under the review summary, a single heading outline through the cart drawer, the subscription add-to-cart control in the slot the buy box lays out for it after the quantity selector, and the theme's own type and spacing on the purchase-option selector inside a subscription widget's shadow root. None of the four is a setting. All four are markup, and the markup arrives after the page does.
The Constraint
None of the markup in question is authored by the theme. It's injected by vendor scripts after load and again on state changes — a variant switch, a drawer open, a cart update. Heading levels aren't part of the configuration either widget exposes, and the subscription widget is a custom element whose internals sit in a shadow root ordinary selectors don't reach. The apps couldn't be changed; they're the client's purchases, so the fixes had to live in the theme.
That fixes the shape of any correction. It has to be applied reactively, because there's no moment at which the vendor's markup is final. It has to survive re-renders, because the vendor will re-emit the original markup the next time its state changes. And it has to be idempotent, because a routine that watches the document for changes will be woken by the change it just made. A correction that isn't safe to run against its own output is a loop.
What We Built
The corrections live at the end of theme.liquid, so they load on every template after the vendor scripts have had their chance, plus one routine at the end of the theme's theme.js bundle that belongs to the product form.
One heading under the review summary, in two passes. convertYotpoTitleToH2() finds .yotpo-paragraph-summary-title, and if it isn't already an <h2>, creates one, copies the original's className and innerHTML across, and replaces the node. A second routine finds .yotpo-paragraph-summary-title-container; when that container is an <h3>, it builds a <div>, copies every attribute from the container onto it, moves all the child nodes across, and swaps it in — so the title inside is the only heading the summary contributes to the outline. Both run once on load and again from a MutationObserver whenever the widget re-renders.
One outline through the cart drawer. An observer walks #rebuy-cart h4, #rebuy-cart h5 and replaces each with a <div> carrying the same class and content. The vendor's styling, which is bound to those classes, is untouched; only the outline changes.
The subscription add-to-cart, re-anchored. In theme.js, an observer watches .product__submit__buttons for added nodes. When one carries yotpo-widget-subscriptions-add-to-cart, it's moved to sit directly after .selector-wrapper--qty, the quantity control — the slot the buy box was laid out for.
Theme styling across the shadow boundary. On product templates only, styleRechargeShadow() queries recharge-subscription-widget, reads its shadowRoot, and — if no element with the id recharge-force-style is already there — appends a <style> with that id setting the font weight on .rc-purchase-option__selector and the padding and flex alignment on .rc-purchase-option__label. It runs immediately and again two seconds later, because the widget mounts asynchronously and may not exist on the first pass.
The convergence property is the design, not a detail. The Yotpo title fix checks tagName before acting. The Recharge fix checks for its own style id. The heading replacements produce <div>s, which h4, h5 and the <h3>-container test no longer match. Each observer sees its own mutation and finds nothing to do.
Why This Way
A one-shot fix on DOMContentLoaded would be correct until the first variant change re-renders Yotpo's summary or the first drawer open re-renders Rebuy's headings. Reacting to mutations is the only way a correction outlives the vendor's next render, and once you're reacting to mutations, idempotency is the thing that keeps the page from spinning.
Rewriting a heading to a <div> rather than removing it preserves the vendor's classes and therefore its styling — the shopper sees the same drawer, and the outline the page publishes is the theme's. Copying attributes wholesale in the Yotpo container fix means whatever the vendor hangs on that element survives the swap. Injecting a stylesheet into Recharge's shadow root is the only way inside a shadow DOM the theme doesn't own; the two-second re-run exists because the widget mounts asynchronously.
One thing we'd rather say than have you discover: this is theme code doing a job that isn't exposed as configuration, so it belongs to the theme from here on. The client's own task register carries this work as maintenance rather than as a project, and that's the right ledger for it.
Implementation Notes
- Yotpo summary title: the title node is replaced with an
<h2>carrying itsclassNameandinnerHTML; the container is replaced with a<div>with every attribute copied via[...container.attributes].forEachand the children moved withappendChild. - Rebuy cart:
#rebuy-cart h4, #rebuy-cart h5swapped for<div>elements preservingclassNameandinnerHTML, bound onDOMContentLoadedbecause the drawer mounts after theme boot. - Recharge:
<style id="recharge-force-style">appended towidget.shadowRoot, guarded byshadowRoot.getElementById, run at once and again after 2000 ms — emitted only inside{% if request.page_type == 'product' %}. - Yotpo subscription add-to-cart: the
theme.jsobserver watches.product__submit__buttonsforchildListadditions and moves theyotpo-widget-subscriptions-add-to-cartnode after.selector-wrapper--qtywithinsertBefore(node, target.nextSibling). - Both Yotpo heading routines call themselves once at load before installing their observer, so a widget that rendered before theme boot is corrected without waiting for a mutation.
Edge Cases
- Every fix is idempotent by construction: a
tagNamecheck, a style-id check, or a replacement node that no longer matches the observer's own selector. - The Recharge routine returns early when the widget or its
shadowRootdoesn't exist, so a product with no subscription option runs none of it. - The
theme.jsobserver returns early — and logs — when.product__submit__buttonsis absent, so the rest of the bundle is unaffected on non-product templates. - A Yotpo re-render that arrives already as an
<h2>is left alone; the check is on the current tag, not on whether the routine has run before. - A drawer re-render that emits fresh
<h4>elements is converted again on the next mutation, because the fix is on the node, not a flag.
Platform Primitives Used
- Theme architecture — the layout wraps every template, so a correction installed at the end of
theme.liquidreaches every instance of every widget without a per-template mount. - Theme app extension blocks — Recharge's widget arrives as an app block rendering a custom element with a shadow root; the style injection is aimed at that boundary.
Integrations in Play
- Yotpo — its review summary heading is re-levelled, and its subscription add-to-cart control re-anchored inside the buy box. The widgets are Yotpo's; the corrections sit alongside them.
- Rebuy — the cart drawer's heading levels are demoted to
<div>s with their classes intact. The drawer is Rebuy's and is otherwise untouched. - Recharge — its subscription widget is styled from inside its own shadow root so the purchase-option selector takes the buy box's type and spacing. The widget is Recharge's; the stylesheet is ours.
Where It Runs
On the product detail page for the Recharge injection and the Yotpo add-to-cart relocation, in the cart drawer for the Rebuy heading fix, and globally for the Yotpo summary routines, which run wherever the widget renders. There's no template list because the mount is the layout; the product-page routines gate themselves on page type or on the product form's presence.
What This Demonstrates
- Subscription purchase options on the product page — the primary capability: the widget-placement and styling work a subscription app's own UI needs once it's in the buy box, here for two vendors at once.
- Vendor widget remediation and cross-app cart sync — observer and shadow-DOM layers that re-level, re-anchor and restyle bought widgets; this build is the clearest single instance of it on this storefront.
- Review platform integration and migration — the review vendor's summary heading is part of the same layer.
How We Know
Two files read from the theme — the layout, where four of the routines live, and the theme's JavaScript bundle, where the product-form observer lives — plus four entries in the client's task register. The register lists work on these same vendors as ongoing items: subscription-widget layout, review styling, the Recharge widget alongside them. It records them as maintenance rather than as a project, which is the honest frame for this page too. Confidence is strong on what the code does; the register supplies the why.
Related Builds
- Configurable PDP block system on the Palo Alto theme — the same storefront's buy box, where Recharge's app block is captured and re-emitted in Liquid; this page is what happens to that widget after it mounts.
- GA4 ecommerce dataLayer instrumentation across a Rebuy cart — the same drawer, listened to rather than corrected.
- Subscription bundle grid with tiered unlock progress — Three Ships again: the theme rendering its own selling-plan control instead of remediating a vendor's.
- Country/market selector hardened for three simultaneous instances — LUS Brands. Heading and role corrections made in the markup itself, because there the markup was the theme's to change.
The Buy-vs-Build Question
Every one of these routines is the cost of a buy decision, paid after the subscription fee. The reviews, the drawer and the subscriptions are all bought on purpose, and each one needed theme code to sit properly in this page — code that is ours to keep. That accounting is the subject of Checkout extension and theme app conflict debugging: buy or build?
Provenance & Evidence
- Client: Three Ships — threeshipsbeauty.ca
- Surfaces: Product detail page, cart drawer, and global for the routines in the layout
- Templates served: none listed; the routines mount from the layout and gate themselves by page type or by the presence of the product form
- Complexity: Medium — roughly 170 lines across the layout and the bundle
- Attribution: Deploi-authored. The observer routines, the shadow-root stylesheet and the relocation are ours. They run inside Palo Alto 5.8.0 by Presidio Creative, a paid premium theme the brand licenses, against markup rendered by Yotpo, Rebuy and Recharge. The widgets themselves are the vendors'.
- Status: Live, verified 2026-09-07
- Evidence: Two theme files read directly, plus four task-register entries
- Confidence: Strong — the code is explicit about each correction, and the register records the same vendors as ongoing maintenance
- Primary capability: Subscription purchase options on the product page
Ready to Make Bought Widgets Behave Like Part of Your Page?
Reviews, subscriptions and a smart cart are worth buying. Getting them to share a heading outline and a type scale with the page they're on is the part nobody quotes for. Contact us today and we'll read what your widgets render and tell you which corrections belong in the theme.