Prompt and scope
You must ship an <account-summary> custom element for hosts rendered by React, Vue, or server templates. It displays account status, reacts to a status attribute, dispatches a public event, and releases subscriptions when removed from the document. Styles must stay contained without losing keyboard or assistive-technology semantics.
Assume an autonomous custom element first. If the requirement is to extend a native button or another built-in, evaluate customized built-in elements separately because browser support changes the design. The interview tests lifecycle boundaries and resource ownership, not framework component syntax.
What the interviewer is evaluating
- Whether you can give constructor, connectedCallback, disconnectedCallback, and attribute observation distinct responsibilities.
- Whether you handle reconnects, moves, upgrades, and initial attributes instead of assuming each callback runs once.
- Whether you connect Shadow DOM encapsulation with accessibility, event propagation, and host integration.
- Whether you can propose idempotent registration, compatibility, cleanup, and tests.
Reciting callback names is not enough. A strong answer defines resource ownership first, then explains how connection, attribute, and render state interact.
Questions to clarify first
- Must the element extend a native semantic element? If so,
issyntax and browser support affect the choice. - Are inputs strings, or must callers pass objects and callbacks? Complex values need an explicit property or method contract.
- Must state survive a move within the same document? This determines whether to assess
connectedMoveCallbackor explicitly preserve state. - Must events cross the Shadow DOM boundary? Agree on
bubbles,composed, and payload before choosing the event API.
A 30-second answer framework
“I separate definition, connection, attribute synchronization, rendering, and cleanup. The constructor only initializes lightweight fields and does not depend on child nodes or the external document. connectedCallback establishes subscriptions and renders idempotently; disconnectedCallback releases listeners, timers, and observers. I observe only required attributes, normalize their string values in attributeChangedCallback, and route updates through one render path. Shadow DOM contains implementation styles, while public semantics, focus, and events remain part of the tested host contract. I finish with tests for reconnects, moves, initial attributes, removal, upgrades, and unsupported browsers.”
Step-by-step analysis
1. Define state and resource ownership
Track connected, subscribed, and rendered state separately. The constructor should not read outside DOM or start network work: a browser may construct an element before connecting it to a document.
class AccountSummary extends HTMLElement {
static observedAttributes = ["status"];
#connected = false;
#unsubscribe = null;
#root;
constructor() {
super();
this.#root = this.attachShadow({ mode: "open" });
}
}2. Make connectedCallback safe to repeat
An element can be removed and inserted again, so connectedCallback must not register another listener unconditionally. Check connection state, establish subscriptions, and make rendering safe to invoke repeatedly.
connectedCallback() {
if (this.#connected) return;
this.#connected = true;
this.#unsubscribe = accountStore.subscribe(() => this.#render());
this.#render();
}3. Release every side effect in disconnectedCallback
Event listeners, setInterval, ResizeObserver, AbortController, and store subscriptions need explicit owners. Clear references after cleanup so a later connection creates exactly one fresh set of resources.
disconnectedCallback() {
this.#unsubscribe?.();
this.#unsubscribe = null;
this.#connected = false;
}Older move patterns can trigger disconnect and connect callbacks in sequence. If preserving state across moves matters, assess connectedMoveCallback where supported, or rebuild from attributes and durable state rather than relying on callback ordering.
4. Observe attributes and separate properties
observedAttributes should list only values that need synchronization. attributeChangedCallback can run for an initial attribute during parsing, so “changed” does not necessarily mean a user just edited it. Handle initial and later values through the same path and avoid writing the same attribute back from its callback.
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
if (name === "status") this.#renderStatus(newValue ?? "unknown");
}String attributes work well for declarative HTML. Pass arrays, objects, and callbacks through documented properties or methods instead of treating arbitrary JSON strings as an unlimited protocol.
5. Design Shadow DOM and the host contract
Shadow DOM isolates internal CSS and DOM queries, but it does not supply semantics automatically. Use correct roles, accessible names, focus order, and visibility. If the host must participate in layout, expose a narrow contract through :host, slots, or CSS custom properties.
:host { display: block; }
:host([hidden]) { display: none; }For events that hosts must observe, set bubbles and composed deliberately and keep the payload stable. Crossing a boundary does not require exposing internal nodes.
6. Handle registration, upgrades, and compatibility
A name can be defined only once in a global registry. Shared libraries should use a namespace prefix and check customElements.get(name) before defining; do not hide collisions by catching an exception. Elements parsed before registration are upgraded after definition, so the implementation must support that order.
const name = "account-summary";
if (!customElements.get(name)) {
customElements.define(name, AccountSummary);
}Autonomous elements are usually easier to deploy across browsers. MDN documents Safari limitations for customized built-in elements; if extending a native element is mandatory, verify the target browser matrix and prepare a fallback.
High-quality sample answer
I would write an ownership table first: the constructor creates the Shadow Root and default fields, connectedCallback subscribes and renders, disconnectedCallback unsubscribes, and attributeChangedCallback handles only observed declarative configuration. Each phase is repeatable or safely skippable; “runs once” is not treated as a lifecycle guarantee.
I would choose an autonomous custom element and guard registration with customElements.get. Shadow DOM contains implementation styles, while the component still exposes an accessible name, focus behavior, and event contract. Cross-boundary events use explicit bubbles and composed settings with a constrained payload. Attributes carry primitive declarative configuration; complex objects use properties or methods.
Verification covers parse-before-define upgrades, initial attributes, reconnects, moves, cleanup, cross-boundary events, keyboard behavior, and screen-reader semantics. If customized built-ins are required, I would validate the browser matrix first, then choose that path only if the compatibility cost is acceptable.
Common mistakes and improvements
- Mistake → Read child nodes or start requests in the constructor → Why it fails → The element may not be connected yet → Improvement → Move document and network work to connectedCallback and bind requests to an AbortController.
- Mistake → Add listeners on every connectedCallback → Why it fails → Reconnects duplicate work and leak memory → Improvement → Keep a subscription handle and make setup/cleanup idempotent pairs.
- Mistake → Unconditionally call setAttribute from the attribute callback → Why it fails → It can form a callback loop, and parsing initial attributes also invokes the callback → Improvement → Compare old and new values, normalize once, and avoid writing the same attribute.
- Mistake → Treat Shadow DOM as an accessibility solution → Why it fails → Encapsulation does not create names, focus, or semantics → Improvement → Test the public contract with keyboard and assistive technology flows.
Follow-up questions and responses
The element is parsed before it is defined. How do you avoid a flash?
Treat the undefined element as an upgradeable state and provide readable fallback content or a loading state. The browser upgrades instances after registration. If code must wait, the host can await customElements.whenDefined("account-summary"), but the whole page should not block on the component script.
Should complex objects be placed in attributes?
Usually no. Attributes are strings and fit serializable declarative configuration; objects and callbacks should use documented properties or methods with clear pre- and post-connection behavior. If serialization is required, specify version, size, and parse-error handling.
Why does a host not receive a click from a button inside Shadow DOM?
Cross-boundary propagation depends on composed; upward bubbling depends on bubbles. Dispatch a stable semantic event with both options chosen explicitly. Outside the boundary, the target may be retargeted to the shadow host, so hosts should not depend on internal nodes.
How do you prove reconnects do not leak?
Insert and remove the same instance repeatedly, count subscriptions, event deliveries, and timer handles, then use memory snapshots to verify old nodes can be collected. Cover initial attribute callbacks, moves, and parse-before-define upgrades; first render alone proves little.