Web Components no longer need JavaScript to show their content
Summary
Component content that used to appear only after scripts ran can now arrive in the HTML response, and every major browser supports it. That puts the text in front of any crawler that skips JavaScript, and DebugBear, a speed-testing tool, notes that Bing is one of them often enough to matter.
Check view-source for text inside your components. If it is missing, have the server output the template markup, and make sure component scripts reuse that markup instead of rebuilding it.
Web Components can now send their full structure and styles as plain HTML, with no JavaScript needed to display them. DebugBear’s guide to Declarative Shadow DOM, written by Anna Monus and published 2026-04-29, explains how it works. The server writes a <template> element with a shadowrootmode attribute, and the browser turns it into a component while it parses the page. DebugBear, which sells page speed monitoring, notes that the feature has worked in all major browsers since February 2024.
Web Components are custom HTML elements, such as <user-card>, built from standard browser features rather than a framework like React. Their internals usually live in a shadow root, which is a walled-off section of the page whose markup and styles stay separate from everything else. Until Declarative Shadow DOM arrived, the only way to create a shadow root was a JavaScript call, attachShadow(), so a server had no way to write one into HTML. The component’s content stayed invisible until the browser had downloaded and run the script bundle.
With Declarative Shadow DOM, the server writes the shadow root out as markup. The example below comes from DebugBear’s post:
<user-card>
<template shadowrootmode="open">
<style>/* ... */</style>
<img src="jane.jpg" alt="Jane Smith" />
<h2>Jane Smith</h2>
<p>Lead Engineer</p>
</template>
</user-card>
When the parser reaches the template, it moves the children into a shadow root and removes the <template> element. The card has its structure and styles from the first render. JavaScript can still add behaviour later, and anything interactive still needs it. Any tool that outputs HTML can produce this markup, including Eleventy, Astro, Django, Rails and Laravel.
Core Web Vitals and crawlers
DebugBear argues that the feature helps all three Core Web Vitals. The component’s content is in the first HTML response, so when a component holds the page’s largest element, no script delays Largest Contentful Paint (LCP), the time until the biggest visible element appears. Layout is stable from the start, which removes the shifts that client-side rendering causes and so helps Cumulative Layout Shift (CLS). Shipping less JavaScript also cuts the time the browser spends unable to respond to taps and clicks, which is what Interaction to Next Paint (INP) measures. Google’s web.dev article on the feature describes the old problem from the other side. Attaching shadow roots to elements that had already rendered without them could shift the layout or briefly show unstyled content.
For SEO, crawlers are the bigger reason to care. DebugBear points out that Google renders JavaScript fully, but Bing’s JavaScript rendering is inconsistent, so a Web Component built in the browser may not be indexed at all. With Declarative Shadow DOM, the text is in the raw HTML that every crawler fetches. The post does not discuss AI crawlers, but the same reasoning applies to any bot that reads raw HTML without running scripts.
The better bet is to treat component rendering as a normal server-side rendering question. If a component carries content you want indexed, that content should arrive in the HTML response.
What to do
- Check whether your component content is in the raw HTML. Use view-source, not the DevTools Elements panel, because the Elements panel shows the page after scripts have run. Search for a line of text that sits inside a component. If it is missing, crawlers that skip JavaScript will not see it either.
- Have your server or build step output the
<template shadowrootmode="open">block. Each instance of a component needs its own template, which makes the markup verbose. DebugBear suggests generating it with a loop in your template engine.
Watch out for
Older component scripts can throw away the server-rendered content. According to web.dev, calling attachShadow() on an element that already has a declarative shadow root does not raise an error. Instead it empties that root and returns it, and the script then rebuilds the content in the browser, so the page depends on JavaScript again. The web.dev article recommends checking for an existing root in the component’s constructor first:
class UserCard extends HTMLElement {
constructor() {
super();
const internals = this.attachInternals();
let shadow = internals.shadowRoot;
if (!shadow) {
// No server-rendered root arrived, so build one in the browser
shadow = this.attachShadow({ mode: "open" });
}
}
}
Form controls should stay outside the shadow root. DebugBear notes that ARIA references such as aria-labelledby cannot point across the shadow boundary. A <form> outside the shadow root also cannot see an <input> inside it, so submission and validation break. Keep inputs, buttons and labels in the regular page markup and pass them in with <slot>.
Not every new Web Components feature follows the same path. Scoped custom element registries, which arrived in Chrome and Edge 146, only work through JavaScript. Check the view-source result again whenever your component library adopts one of them.