Web components have had a quiet gap for years: you could server-render a component's shadow tree with Declarative Shadow DOM, but only if you were happy with named slotting. The moment a component needed manual slot assignment — deciding at runtime which nodes go into which slot — you were forced back into JavaScript. Chrome 151, stable since July 28, 2026, closes that gap with a new shadowrootslotassignment attribute on the <template> element.
It's a small addition with an outsized effect on how cleanly you can ship interactive, server-rendered components. Here's the whole story.
A quick refresher on the two slotting modes
A slot is a placeholder inside a component's shadow tree that light-DOM children get projected into. There are two ways the browser decides which child lands in which slot.
Named assignment is the default and the one most people know. Children carry a slot attribute, and the browser matches them to a <slot> with the same name:
<my-card>
<h2 slot="title">Quarterly report</h2>
<p>Revenue is up across every region.</p>
</my-card>
Inside the component's shadow root, <slot name="title"> collects the heading, and the default <slot> collects everything else. The matching is declarative and attribute-driven — the browser does it for you.
Manual assignment hands that decision to you in script. You create the shadow root with slotAssignment: "manual" and then call HTMLSlotElement.assign() with the exact nodes you want in each slot:
const shadow = host.attachShadow({ mode: "open", slotAssignment: "manual" });
shadow.innerHTML = `<slot id="a"></slot><slot id="b"></slot>`;
const [slotA, slotB] = shadow.querySelectorAll("slot");
slotA.assign(host.children[0]);
slotB.assign(host.children[1]);
Manual mode exists because attribute-based matching isn't always enough. You might not control the markup of the children, so you can't add slot attributes to them. You might need to move a node between slots based on state — a list item that jumps from "inbox" to "archived" — without mutating its attributes. Or you might want slotting logic that depends on runtime data rather than static markup. The HTMLSlotElement.assign() API makes the shadow tree the single source of truth for projection.
Declarative Shadow DOM, and where it stopped short
Declarative Shadow DOM (DSD) lets the server send a shadow root as HTML, so a component renders correctly before any JavaScript loads. You nest a <template> with a shadowrootmode attribute inside the host element, and the parser attaches the shadow tree during HTML parsing:
<my-card>
<template shadowrootmode="open">
<style>/* scoped styles */</style>
<slot name="title"></slot>
<slot></slot>
</template>
<h2 slot="title">Quarterly report</h2>
<p>Revenue is up across every region.</p>
</my-card>
This is the backbone of server-side rendering for web components. It removes the flash of unstyled or unstructured content, improves the Largest Contentful Paint story, and lets a component be meaningful to crawlers and to users on slow connections — all without waiting on hydration.
But DSD only ever supported named slotting. There was no HTML equivalent of slotAssignment: "manual". If your component relied on manual assignment, its shadow tree could not be expressed declaratively; you had to attach the shadow root imperatively with attachShadow() after the page loaded. That defeats the purpose of DSD for exactly the components that are often the most dynamic. Server rendering and manual slotting were mutually exclusive.
What Chrome 151 adds
The fix is a single attribute. Per the Chrome 151 release notes and the ChromeStatus entry, Chrome 151 adds the shadowrootslotassignment attribute to <template>. It mirrors the imperative slotAssignment option and accepts two values:
named— the default, preserving today's behavior.manual— the declarative equivalent ofattachShadow({ slotAssignment: "manual" }).
The attribute is defined in the WHATWG HTML specification and reflected by the shadowRootSlotAssignment property on HTMLTemplateElement, so it behaves like the other declarative shadow-root knobs (shadowrootmode, shadowrootclonable, shadowrootdelegatesfocus, and shadowrootserializable).
Here's a manual-assignment shadow root delivered entirely as HTML:
<message-thread>
<template shadowrootmode="open" shadowrootslotassignment="manual">
<style>
slot { display: block; }
#pinned { border-left: 3px solid var(--accent, #E84B1A); }
</style>
<slot id="pinned"></slot>
<slot id="rest"></slot>
</template>
<div>Kickoff moved to Thursday.</div>
<div>Please review the SOW before the call.</div>
<div>Invoice #204 was paid.</div>
</message-thread>
At this point the two <slot> elements are empty — manual mode means nothing is projected until something calls assign(). Your component's connectedCallback runs that logic once, taking over a shadow tree the server already painted:
class MessageThread extends HTMLElement {
connectedCallback() {
// The shadow root already exists from DSD — don't re-create it.
const shadow = this.shadowRoot;
const pinned = shadow.getElementById("pinned");
const rest = shadow.getElementById("rest");
const messages = [...this.children];
const pinnedIds = new Set(this.pinnedIds ?? []);
pinned.assign(...messages.filter((m) => pinnedIds.has(m.id)));
rest.assign(...messages.filter((m) => !pinnedIds.has(m.id)));
}
}
customElements.define("message-thread", MessageThread);
Notice what did not change: the light-DOM <div>s carry no slot attributes, and the component never rewrites them. Projection is decided in script and re-decided whenever state changes, while the initial markup still arrives fully formed from the server.
Reading it back and feature detection
Because the attribute is reflected, you can inspect it before doing anything expensive:
const tpl = document.querySelector("message-thread template");
tpl?.shadowRootSlotAssignment; // "manual"
For progressive enhancement, detect support and fall back to imperative attachment on browsers that don't yet parse the attribute:
const supportsManualDSD =
"shadowRootSlotAssignment" in HTMLTemplateElement.prototype;
if (!supportsManualDSD) {
// Older engine: attach the shadow root yourself with slotAssignment: "manual".
}
A word of caution on polyfilling DSD: if you convert <template shadowrootmode> elements to shadow roots manually for older browsers, remember to pass slotAssignment: "manual" when you see shadowrootslotassignment="manual", or your slots will silently fall back to named matching and project nothing.
Where this actually helps
The clearest win is any server-rendered component whose composition is decided by data rather than by static slot attributes:
- Layouts that sort or partition children — pinned versus unpinned messages, featured versus regular cards, valid versus invalid form rows — where the grouping comes from application state.
- Wrappers around third-party or user-authored markup you can't annotate with
slotattributes but still need to place into named regions. - Design-system primitives that want one server-rendered HTML contract regardless of whether the consumer ends up using named or manual slotting under the hood.
For these, Chrome 151 lets you ship the same declarative HTML you already ship for named components and layer the assignment logic on top during hydration — instead of maintaining a separate, JS-only rendering path for the manual-slot cases.
Browser support and how to adopt it
This landed in Chrome 151 stable and, by extension, Chromium-based browsers on the same version line. As of this writing it is a Chromium feature, not yet Baseline, so treat it as a progressive enhancement rather than a hard dependency: server-render your DSD as usual, feature-detect shadowRootSlotAssignment, and keep the imperative attachShadow({ slotAssignment: "manual" }) path for engines that haven't shipped it. Check current cross-browser status on MDN or caniuse before relying on it in production, and gate it behind detection either way.
The takeaways are simple. Declarative Shadow DOM no longer forces a choice between server rendering and manual slot assignment. If you maintain a web-component design system, audit any component that calls attachShadow purely to get slotAssignment: "manual" — those are now candidates for a fully declarative shadow tree. And whatever you adopt, keep the feature detection in place until the attribute is Baseline across engines, so the components degrade to imperative attachment instead of rendering empty slots.