Skip to content

Composing Components

The simplest form of composition is nesting: an outer custom element creates one or more inner custom elements inside connectedCallback. Define the inner elements first so the registry knows about them before the outer element tries to use them.

// Inner element — defined first
class InnerWidget extends HTMLElement {
connectedCallback() {
this.textContent = 'I am the inner widget';
}
}
customElements.define('inner-widget', InnerWidget);
// Outer element — creates the inner one programmatically
class OuterWidget extends HTMLElement {
connectedCallback() {
var inner = document.createElement('inner-widget');
this.appendChild(inner);
}
}
customElements.define('outer-widget', OuterWidget);

Place the outer element in HTML and the browser handles the rest:

<outer-widget></outer-widget>

The most straightforward way for a parent element to pass data to a child is through HTML attributes. The parent calls setAttribute on the child element it creates; the child reads the value with getAttribute in its own connectedCallback.

// Parent sets the child's attribute
var child = document.createElement('name-tag');
child.setAttribute('name', this.getAttribute('name') || 'friend');
this.appendChild(child);
// Child reads it
NameTag.prototype.connectedCallback = function() {
var name = this.getAttribute('name') || 'stranger';
this.textContent = name;
};

Attributes are always strings, so convert to the type you need (for example, parseInt for numbers).

When a child element needs to tell its parent that something happened — a button was clicked, a value was selected — it dispatches a CustomEvent. The parent listens for that event in its own connectedCallback.

Two options on the event constructor matter here:

  • bubbles: true — the event travels up the DOM tree past the child
  • composed: true — the event crosses shadow-DOM boundaries
// Child dispatches an event
this.addEventListener('click', function() {
self.dispatchEvent(new CustomEvent('item-selected', {
bubbles: true,
composed: true,
detail: { value: self.getAttribute('value') }
}));
});
// Parent listens in connectedCallback
OuterWidget.prototype.connectedCallback = function() {
var self = this;
// ... build children ...
self.addEventListener('item-selected', function(e) {
console.log('selected:', e.detail.value);
});
};

Because bubbles: true is set, the parent can attach a single listener to itself and receive events from any child nested inside it — no direct reference needed.

The demo below composes a <star-rating> from five <rating-star> elements. Each star listens for a click and dispatches a star-select CustomEvent. The outer <star-rating> listens for those events and updates a display span with the chosen rating.

What property on CustomEvent options makes the event cross shadow-DOM boundaries?
In the star-rating demo, which element dispatches the star-select event?
Which method does the parent element use to subscribe to child events in connectedCallback?