Shopify Web Components in 2025 — The Complete Architecture Guide for Theme Devs
Shopify's Dawn theme introduced Web Components as the primary JavaScript architecture pattern in 2021, replacing the jQuery-based components used in older themes. Horizon takes this further with strict section isolation requirements. Understanding Web Components is now essential for any Shopify theme developer — not just for building new themes, but for debugging, extending, and maintaining existing ones. This guide covers the complete architecture: custom element lifecycle, state management, event communication, and the common mistakes that cause memory leaks.
What Web Components are and why Shopify uses them
Web Components are a set of browser APIs — Custom Elements, Shadow DOM, and HTML Templates — that allow you to define reusable HTML elements with encapsulated behavior. Shopify themes use the Custom Elements API primarily: class ProductForm extends HTMLElement { ... } followed by customElements.define('product-form', ProductForm). The browser instantiates the class when the element appears in the DOM and calls lifecycle callbacks as it is connected, disconnected, or has attributes changed. This model maps well to Shopify sections — each section can be a self-contained custom element.
The four lifecycle callbacks you must know
connectedCallback() is called when the element is inserted into the DOM — initialize event listeners, fetch data, and set up state here. disconnectedCallback() is called when the element is removed from the DOM — this is where you MUST clean up all event listeners, clear intervals, cancel fetch requests, and disconnect observers. This cleanup is not optional — missing it causes memory leaks that degrade store performance over time. attributeChangedCallback(name, oldValue, newValue) is called when observed attributes change — use for reactive attribute-driven behavior. constructor() is called when the element is created — never query the DOM here, as the element's children may not be attached yet.
Always query the DOM in connectedCallback, never in constructor
A common Web Component bug: querying child elements in the constructor, getting null, and initializing incorrectly. The element's children are not attached when the constructor runs. Wait for connectedCallback: class HeroSlider extends HTMLElement { connectedCallback() { this.slides = this.querySelectorAll('.slide'); this.currentIndex = 0; this.setupControls(); } }. Note the use of this.querySelectorAll rather than document.querySelectorAll — always scope queries to the component using this to avoid selecting elements from other components.
Using customElements.get() before defining to prevent errors
Shopify sections can render on the same page multiple times. If the same section JavaScript is included twice, customElements.define() throws an error when called with an already-registered name. Always check before defining: if (!customElements.get('product-form')) { customElements.define('product-form', ProductForm); }. Dawn and Horizon both use this pattern. Without it, loading the same section JavaScript twice causes a NotSupportedError that breaks the entire component.
The fix: before and after
// CODE_COMPARISON
Frequently asked questions
- Do I need to use Shadow DOM for Shopify Web Components?
No. Shopify themes typically use Custom Elements without Shadow DOM. Shadow DOM creates a style and DOM boundary that makes it harder to integrate with theme CSS and Shopify's Theme Editor. Dawn and Horizon both use Custom Elements with regular (light) DOM. Shadow DOM is useful for fully isolated components like third-party widgets, but for theme components, regular Custom Elements are the correct choice.
- When is disconnectedCallback called?
disconnectedCallback is called when the element is removed from the DOM. In Shopify themes, this happens when the Theme Editor removes or replaces a section, when Shopify's Section Rendering API replaces section HTML, and when page navigation occurs in Hydrogen. In classic Liquid themes, disconnectedCallback is also called on page unload. Always treat it as a guaranteed cleanup opportunity and never skip it.
- How does this.querySelector differ from document.querySelector in Web Components?
this.querySelector searches only within the custom element's own subtree — it cannot find elements in other components. document.querySelector searches the entire document. In Web Components, always use this.querySelector for finding child elements. If you use document.querySelector, you will find the first matching element anywhere on the page — which may be in a different section, causing cross-component contamination.
// SCAN_YOUR_CODE
Does your theme have this bug?
Paste your code. Syphio automatically detects and fixes this error and hundreds of others — in seconds.
Check my JavaScript →