Prompt and scope
The page streams product cards from the server. Each card should render meaningful content without JavaScript, keep component styles isolated, and attach behavior when the client bundle arrives. Use a declarative shadow root, then describe progressive enhancement for browsers that do not parse the attribute and for custom elements that upgrade after the HTML is present.
The core skill is browser rendering, component boundaries, and hydration correctness, so this belongs to frontend.
What interviewers assess
First, do you know that a template carrying shadowrootmode="open" or "closed" is converted by the HTML parser into a shadow root when supported?
Second, can you explain that only the first declarative shadow root for a host is attached; later ones remain templates and need an intentional fallback strategy?
Third, can you preserve slots, styles, accessibility, and form behavior across server HTML and client upgrade?
Fourth, can you distinguish open inspection from closed encapsulation? Closed mode hides the shadowRoot reference; it is not a security boundary.
Fifth, can you prevent double rendering, duplicate listeners, and lost events during hydration or streaming?
Questions to clarify first
- Which browsers and server-rendered response modes are supported?
- Does the host become a custom element immediately, or can it upgrade later?
- Which children are slotted, and must consumers style them with
::slotted? - Is open mode required for testing and integration, or is closed mode a product constraint?
- Does the page stream nested components or send the complete root in one response?
- What is the fallback when declarative parsing is unavailable?
30-second answer framework
“I would server-render the component with a declarative shadow root and semantic light-DOM fallback, then let the custom element upgrade attach behavior without recreating the already parsed root. I would use slots for public content, keep styles inside the root, choose open or closed mode as an integration decision rather than a security control, and feature-detect shadowRootMode. Hydration is idempotent, events are delegated or bound once, and tests cover unsupported browsers, streaming order, slots, accessibility, and upgrade timing.”
Step-by-step answer
Step 1: Render a semantic root
The server emits a host with a declarative template. Keep the text and controls meaningful so the response remains useful before JavaScript. Use open when tooling or integration needs inspection; use closed only when the component contract intentionally hides the reference.
<product-card>
<template shadowrootmode="open">
<style>:host { display: block }</style>
<article><slot name="title"></slot><button>Buy</button></article>
</template>
<span slot="title">Keyboard</span>
</product-card>The slot assignment is part of the public contract. Do not duplicate the title inside both the shadow tree and fallback content unless you also control accessibility semantics.
Step 2: Define the upgrade contract
When the custom element class is defined, its lifecycle must detect an existing shadow root rather than calling attachShadow again. Initialize state once, bind listeners once, and leave server-provided nodes in place. If the browser did not create a root, the element can create one from a template or render a compatible light-DOM fallback.
Step 3: Feature-detect and fall back
Check support with a small parser probe or the relevant template property before relying on declarative behavior. Unsupported browsers should still receive semantic light-DOM content; a client-side upgrade may attach a shadow root later, but must not hide content during the transition.
Step 4: Preserve slots and styles
Slots project light-DOM children into the shadow tree. Document named slots, default-slot behavior, and styling limits such as ::slotted. Keep component styles in the root, and expose deliberate custom properties or parts instead of depending on selectors that cannot cross the boundary.
Step 5: Handle streaming and nesting
Streaming can deliver a host before nested slotted children or before the custom-element definition. Treat parsing order as an expected state: the slot should update when children arrive, and upgrade must be safe before or after the stream completes. Avoid replacing the host with a second tree.
Step 6: Hydrate behavior without duplicate work
Attach event handlers once, preferably through a root-level delegate for repeated cards. Mark initialization in a private field or weak map, not a public attribute that consumers can overwrite. Preserve form state and focus when behavior is attached.
Step 7: Test browser and accessibility boundaries
Test supported and unsupported parsers, open and closed modes, one versus multiple declarative roots, slots arriving late, custom-element upgrade before and after parsing, keyboard focus, labels, form submission, and hydration after a network interruption. Assert there is one interactive control tree and no duplicate events.
Model answer
“I would stream a semantic host plus one declarative shadow root, use named slots for public content, and keep styles inside the root. The custom-element upgrade first checks whether a root already exists, so hydration enhances server markup instead of replacing it. Open mode supports inspection; closed mode only hides the reference and is not a security boundary.
I would feature-detect parser support and retain light-DOM fallback content. Streaming and late upgrades are normal states, so slot projection and initialization must be idempotent. Tests cover unsupported browsers, nested roots, late slots, focus, forms, accessibility, and duplicate event prevention.”
Common mistakes
- Calling
attachShadowduring every upgrade → existing roots or state are lost → reuse the parsed root. - Treating closed mode as security → callers can still interact through exposed behavior → document it as encapsulation only.
- Duplicating fallback and shadow content → screen readers may announce twice → define one accessible source.
- Assuming slots arrive before parsing finishes → streamed children disappear from the mental model → test late projection.
- Styling slotted content with internal selectors → rules do not cross the boundary → use
::slotted, parts, or custom properties. - Binding listeners on every render → clicks fire multiple times → make hydration idempotent.
- Dropping light-DOM fallback → unsupported browsers show blank cards → keep semantic server markup.
Follow-up questions
Follow-up 1: Is a declarative shadow root always attached?
No. It depends on parser support and a valid mode. An unsupported browser leaves the template as ordinary content, so a fallback or client upgrade is required.
Follow-up 2: Why only one root per host?
The parser attaches the first declarative root for that host; subsequent templates remain available for intentional handling rather than creating competing roots.
Follow-up 3: When choose open mode?
Choose open mode when tests, integration, or controlled extensions need the root reference. It does not grant or remove security privileges.
Follow-up 4: How do slots work during streaming?
The host can exist before slotted children; once children arrive, the slot assignment updates. Tests should cover both arrival orders.
Follow-up 5: How do you avoid hydration mismatch?
Keep server and client contracts stable, reuse existing nodes, and make initialization idempotent instead of rendering a second tree.
Follow-up 6: How do you test closed roots?
Test user-visible behavior, focus, events, and accessibility through the public contract; do not rely on reading shadowRoot directly.