Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Shopify/horizon/llms.txt
Use this file to discover all available pages before exploring further.
DeclarativeShadowElement
Base class that handles declarative shadow DOM hydration for components mounted after initial page render.
Class Definition
export class DeclarativeShadowElement extends HTMLElement {
connectedCallback() {
if (!this.shadowRoot) {
const template = this.querySelector(':scope > template[shadowrootmode="open"]');
if (!(template instanceof HTMLTemplateElement)) return;
const shadow = this.attachShadow({ mode: 'open' });
shadow.append(template.content.cloneNode(true));
}
}
}
Declarative shadow DOM is automatically initialized on page load. For dynamically added components:
<my-shadow-component>
<template shadowrootmode="open">
<style>
:host { display: block; }
</style>
<slot></slot>
</template>
<p>Content goes here</p>
</my-shadow-component>
class MyShadowComponent extends DeclarativeShadowElement {
connectedCallback() {
super.connectedCallback();
// Shadow root is now available
console.log(this.shadowRoot);
}
}
When to Use
Use DeclarativeShadowElement when you need:
- Encapsulated styles that don’t leak to the page
- Shadow DOM for component isolation
- Support for declarative shadow DOM templates
For most Horizon components, use the Component class instead, which extends DeclarativeShadowElement.
Component
Main base class for Horizon web components with automatic refs management, declarative event handling, and lifecycle hooks.
Class Signature
class Component<T extends Refs = Refs> extends DeclarativeShadowElement {
refs: RefsType<T>;
requiredRefs?: string[];
get roots(): (ShadowRoot | Component<T>)[];
connectedCallback(): void;
updatedCallback(): void;
disconnectedCallback(): void;
}
Properties
Object containing references to child elements with ref attributes. Automatically populated and kept in sync with DOM changes.class MyComponent extends Component {
connectedCallback() {
super.connectedCallback();
// Access refs
this.refs.submitButton.disabled = false;
this.refs.items.forEach(item => console.log(item));
}
}
Array of ref names that must be present. Throws MissingRefError if any are missing.class DialogComponent extends Component {
requiredRefs = ['dialog', 'closeButton'];
}
roots
(ShadowRoot | Component)[]
Read-only property returning the root nodes of the component. If the component has a shadow root, returns both the component and its shadow root. Otherwise, returns just the component.get roots() {
return this.shadowRoot ? [this, this.shadowRoot] : [this];
}
Lifecycle Methods
connectedCallback()
Called when the component is inserted into the DOM.
connectedCallback() {
super.connectedCallback();
registerEventListeners();
this.#updateRefs();
requestIdleCallback(() => {
for (const root of this.roots) {
this.#mutationObserver.observe(root, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['ref'],
attributeOldValue: true,
});
}
});
}
Usage:
class MyComponent extends Component {
connectedCallback() {
super.connectedCallback(); // Always call super first!
// Your initialization code
this.refs.button.addEventListener('click', this.handleClick);
}
}
updatedCallback()
Called when the component is re-rendered by the Section Rendering API.
updatedCallback() {
this.#mutationObserver.takeRecords();
this.#updateRefs();
}
Usage:
class MyComponent extends Component {
updatedCallback() {
super.updatedCallback();
// Refresh component state after section re-render
this.updatePrices();
}
}
disconnectedCallback()
Called when the component is removed from the DOM.
disconnectedCallback() {
this.#mutationObserver.disconnect();
}
Usage:
class MyComponent extends Component {
disconnectedCallback() {
super.disconnectedCallback();
// Clean up event listeners, timers, etc.
this.refs.button.removeEventListener('click', this.handleClick);
}
}
Refs System
Basic Refs
Elements with ref attributes are automatically tracked:
<my-component>
<button ref="submitButton">Submit</button>
<input ref="emailInput" type="email" />
<div ref="container"></div>
</my-component>
class MyComponent extends Component {
connectedCallback() {
super.connectedCallback();
console.log(this.refs.submitButton); // <button>
console.log(this.refs.emailInput); // <input>
console.log(this.refs.container); // <div>
}
}
Array Refs
Use ref="name[]" syntax to collect multiple elements:
<my-component>
<li ref="items[]">Item 1</li>
<li ref="items[]">Item 2</li>
<li ref="items[]">Item 3</li>
</my-component>
class MyComponent extends Component {
connectedCallback() {
super.connectedCallback();
console.log(this.refs.items); // [<li>, <li>, <li>]
this.refs.items.forEach((item, i) => {
console.log(`Item ${i}:`, item.textContent);
});
}
}
Required Refs
Validate that critical refs exist:
class DialogComponent extends Component {
requiredRefs = ['dialog', 'closeButton'];
connectedCallback() {
super.connectedCallback();
// MissingRefError thrown if refs are missing
this.refs.dialog.showModal();
}
}
TypeScript Refs
Type-safe refs with TypeScript:
type Refs = {
submitButton: HTMLButtonElement;
emailInput: HTMLInputElement;
items: HTMLElement[];
};
class MyComponent extends Component<Refs> {
requiredRefs = ['submitButton', 'emailInput'];
connectedCallback() {
super.connectedCallback();
// Fully typed
this.refs.submitButton.disabled = false;
this.refs.emailInput.value = '';
this.refs.items?.forEach(item => item.remove());
}
}
Declarative Event Handling
The Component class automatically sets up event listeners using the on: attribute syntax.
Basic Events
<button on:click="handleClick">Click Me</button>
<input on:input="handleInput" />
<form on:submit="handleSubmit"></form>
<select on:change="handleChange"></select>
class MyComponent extends Component {
handleClick(event) {
console.log('Clicked!');
}
handleInput(event) {
console.log('Value:', event.target.value);
}
handleSubmit(event) {
event.preventDefault();
}
handleChange(event) {
console.log('Selected:', event.target.value);
}
}
Supported Events
const events = [
'click', 'change', 'select', 'focus', 'blur',
'submit', 'input', 'keydown', 'keyup', 'toggle'
];
const expensiveEvents = ['pointerenter', 'pointerleave'];
Target Selector
Specify which component should handle the event:
<!-- Handle on closest component -->
<button on:click="handleClick">Default</button>
<!-- Handle on specific selector -->
<button on:click="my-component/handleClick">On my-component</button>
<!-- Handle on element with ID -->
<button on:click="#myComponent/handleClick">On #myComponent</button>
Passing Data
Pass data to event handlers:
<!-- Single value (number, string, boolean) -->
<button on:click="handleDelete?/3">Delete Item 3</button>
<button on:click="handleToggle?/true">Enable</button>
<!-- Multiple parameters (object) -->
<button on:click="handleAction?id=123&type=delete&confirm=true">Delete</button>
class MyComponent extends Component {
handleDelete(data, event) {
console.log(data); // 3
console.log(event.target);
}
handleToggle(data, event) {
console.log(data); // true
}
handleAction(data, event) {
console.log(data); // {id: 123, type: "delete", confirm: true}
}
}
Mutation Observer
The Component class automatically observes DOM changes to keep refs in sync:
#mutationObserver = new MutationObserver((mutations) => {
if (
mutations.some(
(m) =>
(m.type === 'attributes' && this.#isDescendant(m.target)) ||
(m.type === 'childList' && [...m.addedNodes, ...m.removedNodes].some(this.#isDescendant))
)
) {
this.#updateRefs();
}
});
This means refs are automatically updated when:
- Elements are added or removed
ref attributes change
- Child components are mounted/unmounted
Complete Example
import { Component } from '@theme/component';
/**
* @typedef {object} Refs
* @property {HTMLFormElement} form
* @property {HTMLInputElement} emailInput
* @property {HTMLButtonElement} submitButton
* @property {HTMLElement} errorMessage
* @property {HTMLElement[]} items
*/
/**
* @extends {Component<Refs>}
*/
class NewsletterSignup extends Component {
requiredRefs = ['form', 'emailInput', 'submitButton'];
connectedCallback() {
super.connectedCallback();
// Validate email on input
this.refs.emailInput.addEventListener('input', this.validateEmail);
}
disconnectedCallback() {
super.disconnectedCallback();
this.refs.emailInput.removeEventListener('input', this.validateEmail);
}
validateEmail = () => {
const { emailInput } = this.refs;
const isValid = emailInput.checkValidity();
this.refs.submitButton.disabled = !isValid;
};
// Called via on:submit="handleSubmit"
async handleSubmit(event) {
event.preventDefault();
const { emailInput, submitButton, errorMessage } = this.refs;
submitButton.disabled = true;
try {
const response = await fetch('/newsletter', {
method: 'POST',
body: JSON.stringify({ email: emailInput.value })
});
if (response.ok) {
emailInput.value = '';
this.showSuccess();
} else {
throw new Error('Subscription failed');
}
} catch (error) {
if (errorMessage) {
errorMessage.textContent = 'Please try again later';
errorMessage.classList.remove('hidden');
}
} finally {
submitButton.disabled = false;
}
}
showSuccess() {
// Show success message
}
}
if (!customElements.get('newsletter-signup')) {
customElements.define('newsletter-signup', NewsletterSignup);
}
<newsletter-signup>
<form ref="form" on:submit="handleSubmit">
<input
ref="emailInput"
type="email"
required
placeholder="Enter your email"
/>
<button ref="submitButton" type="submit">Subscribe</button>
<div ref="errorMessage" class="hidden"></div>
</form>
</newsletter-signup>
Helper Functions
Internal helper functions used by the Component class:
getAncestor(node)
Finds the ancestor of a node, traversing shadow DOM boundaries:
function getAncestor(node) {
if (node.parentNode) return node.parentNode;
const root = node.getRootNode();
if (root instanceof ShadowRoot) return root.host;
return null;
}
getClosestComponent(node)
Recursively finds the closest ancestor Component instance:
function getClosestComponent(node) {
if (!node) return null;
if (node instanceof Component) return node;
if (node instanceof HTMLElement && node.tagName.toLowerCase().endsWith('-component')) {
return node;
}
const ancestor = getAncestor(node);
if (ancestor) return getClosestComponent(ancestor);
return null;
}
Error Classes
MissingRefError
Thrown when a required ref is not found:
class MissingRefError extends Error {
constructor(ref, component) {
super(`Required ref "${ref}" not found in component ${component.tagName.toLowerCase()}`);
}
}
Example:
class MyComponent extends Component {
requiredRefs = ['dialog'];
}
// If <dialog ref="dialog"> is missing:
// MissingRefError: Required ref "dialog" not found in component my-component