Skip to main content

DeclarativeShadowElement

Base class that handles declarative shadow DOM hydration for components mounted after initial page render.

Class Definition

component.js
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));
    }
  }
}

Usage

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

refs
RefsType<T>
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));
  }
}
requiredRefs
string[]
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.
component.js
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.
component.js
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.
component.js
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

component.js
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:
component.js
#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:
component.js
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:
component.js
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:
component.js
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

Build docs developers (and LLMs) love