---
title: 'Templates and Slots'
source: 'https://academia.sh/en/courses/browser-platform/templates-and-slots'
course: 'The Browser and the Web Platform'
language: en
updated: '2026-08-17T18:09:12+00:00'
license: 'CC BY-SA 4.0'
---

# Templates and Slots

The markup container that stays unrendered and the rules for copying it; the slot assignment algorithm, fallback content, the flattened tree, and which tree placed content still belongs to.

The previous lesson showed how the shadow tree is isolated, not where its content comes from.
Two open questions remained. First, if the component's internal structure is built node by
node for every instance, fifteen badges run the same setup code fifteen times. Second, the
host's light tree written in the document is nowhere visible: the label and unit written
inside the badge disappear.

This lesson builds two mechanisms together: a markup container that stays unrendered, and the
points in the shadow tree where content from the light tree gets placed.

## An Unrendered Markup Container

There is an element in HTML whose content is parsed but does not enter the document: the
**template**. The parser turns markup inside the template into nodes, but places them not in
the document tree but in a separate document fragment belonging to the template.

The result is that the content is **inert**. An image inside it does not download, a script
inside it does not run, style rules inside it are not applied, its elements take no part in
layout computation. The markup sits written on the page but takes part in none of the page's
behavior.

This is the right container for a component's internal structure. The markup sits readably in
the document; when needed, the component copies the container's content into the shadow tree.
The content is not reparsed from text every time — parsing happened once, everything after is
tree copying.

Reaching a template's content by looking at the element's children gives no result; the
content sits in a separate fragment and is read through it. This surprises on first
encounter: a template element's child node count is zero.

## Cloning and What a Clone Does Not Carry

The copy has to be deep: an operation copying not just the root but the entire subtree. A
shallow copy gives the template's root, not its content.

What the copy does not carry is more instructive than what it does.

**Listeners are not copied.** Even if a listener is attached to a node in the template, the
copy does not get it; a listener is attached after the copied tree has been placed. This is not
a gap but a necessity: a listener is a function reference and is not part of markup.

**Ids are copied.** If an id is written in the template, the same id results in every copy. In
the document tree, this breaks id uniqueness. In the shadow tree, no problem arises: as seen
in the previous lesson, every shadow tree has its own id space. This is why using ids in
templates is tied to the shadow tree.

**Attributes are copied, user state is not.** The distinction from the DOM API lesson applies
here too: the copy gets the initial state the attributes declare; a value the user changed on
the source element does not carry over to the copy.

Copying produces nodes in a fragment not bound to the document. The rule built in the DOM API
lesson works here on its own: no layout computation is triggered while the copy is being
prepared; the computation happens only once, when the fragment is added to the shadow tree.

## Slots and the Assignment Rule

The template gives the component's **own** structure. Content the component receives from
outside comes from somewhere else: the host's light tree. The shadow tree marks the points
where this content gets placed with **slot** elements.

The rule comes down to three points. Only the host's **direct children** can be assigned;
grandchildren are not. A child carrying a `slot` attribute goes to the same-named slot, one
without it goes to the unnamed default slot. A child matching no slot's name is assigned
nowhere and is not rendered.

```js
// slot-assignment.mjs — the slot assignment algorithm and the flattened tree
const element = (name, attrs = {}, ...children) => ({ name, attrs, children, text: null });
const text = (value) => ({ name: "#text", attrs: {}, children: [], text: value });
const slot = (name, ...fallback) => element("slot", name ? { name } : {}, ...fallback);

function* walk(root) { yield root; for (const c of root.children) yield* walk(c); }

const label = (n) =>
  n.text !== null ? `"${n.text}"`
  : n.name === "slot" ? `slot[${n.attrs.name ?? "default"}]`
  : n.name + (n.attrs.slot ? `[slot=${n.attrs.slot}]` : "");

// Assignment: only the host's DIRECT children are assigned; those without a slot attribute
// go to the default slot, those with one go to the same-named slot. Order is the light tree's order.
function assignments(host, shadowRoot) {
  const map = new Map();
  for (const n of walk(shadowRoot)) if (n.name === "slot") map.set(n.attrs.name ?? "", []);
  for (const child of host.children) {
    const wanted = child.attrs.slot ?? "";
    if (map.has(wanted)) map.get(wanted).push(child);
  }
  return map;
}

// Flattened tree: each slot is replaced by its assigned nodes, or by fallback content if unassigned.
function flatten(node, map, depth = 0, lines = []) {
  if (node.name === "slot") {
    const assigned = map.get(node.attrs.name ?? "") ?? [];
    const visible = assigned.length ? assigned : node.children;
    const source = assigned.length ? "light" : "fallback";
    for (const item of visible) {
      lines.push(`${"  ".repeat(depth)}${label(item)}   (${source})`);
      for (const c of item.children) flatten(c, map, depth + 1, lines);
    }
    return lines;
  }
  lines.push(`${"  ".repeat(depth)}${label(node)}`);
  for (const c of node.children) flatten(c, map, depth + 1, lines);
  return lines;
}

// The host's children as written in the document (the light tree)
const host = element("measurement-badge", { id: "temperature" },
  element("span", { slot: "label" }, text("Temperature")),
  element("strong", {}, text("-4.2")),
  element("span", { slot: "unit" }, text("°C")),
  element("span", { slot: "source" }, text("station-3")));

// Shadow tree
const shadowRoot = element("#shadow-root", {},
  element("div", { class: "box" },
    slot("label", text("Measurement")),
    element("output", {}, slot(null, text("—"))),
    slot("unit", text("unitless"))));

const map = assignments(host, shadowRoot);
console.log("assignments:");
for (const [name, nodes] of map)
  console.log(`  slot[${name || "default"}] <-`, nodes.map(label).join(", ") || "(empty -> fallback content)");

const unassigned = host.children.filter((c) => !map.has(c.attrs.slot ?? ""));
console.log("unassigned light nodes:", unassigned.map(label).join(", ") || "(none)");

console.log("\nflattened tree:");
console.log(flatten(shadowRoot, map).join("\n"));

// A light tree that is an empty host, with the same shadow tree: every slot shows its fallback content.
const emptyHost = element("measurement-badge", { id: "humidity" });
console.log("\nlight tree, empty host:");
console.log(flatten(shadowRoot, assignments(emptyHost, shadowRoot)).join("\n"));

// If the light tree changes, assignment is recomputed.
host.children.push(element("em", { slot: "unit" }, text("°F")));
const updated = assignments(host, shadowRoot);
console.log("\nafter adding em[slot=unit] to the light tree:");
for (const [name, nodes] of updated) {
  const before = map.get(name).map(label).join(",");
  const now = nodes.map(label).join(",");
  console.log(`  slot[${name || "default"}] ${before === now ? "unchanged" : "changed -> " + (now || "(empty)")}`);
}
```

```
assignments:
  slot[label] <- span[slot=label]
  slot[default] <- strong
  slot[unit] <- span[slot=unit]
unassigned light nodes: span[slot=source]

flattened tree:
#shadow-root
  div
    span[slot=label]   (light)
      "Temperature"
    output
      strong   (light)
        "-4.2"
    span[slot=unit]   (light)
      "°C"

light tree, empty host:
#shadow-root
  div
    "Measurement"   (fallback)
    output
      "—"   (fallback)
    "unitless"   (fallback)

after adding em[slot=unit] to the light tree:
  slot[label] unchanged
  slot[default] unchanged
  slot[unit] changed -> span[slot=unit],em[slot=unit]
```

The line that stands out in the output's first section is the last one: the child carrying
`slot="source"` matched no slot and so was not assigned, and therefore not rendered. It has not
been deleted from the tree, it still sits in the document; it just never entered the flattened
tree. This means a component user's spelling mistakes disappear silently, and it shows the
component's dependency on documented slot names.

## The Flattened Tree

What the browser actually paints is neither the document tree nor the shadow tree; it is the
shadow tree with every slot replaced by the nodes assigned to it. This is called the
**flattened tree**, and layout and paint are done over this tree.

The output's second section shows this. `slot` elements do not appear in the flattened tree;
nodes from the light tree sit in their place. The `strong` element is a direct child of the
host in the document tree, but sits inside the `output` element when painted. The node's place
in the tree and its place in painting have come apart.

The third section shows the role of **fallback content**. A slot's own children are painted
when nothing is assigned to it. This gives the component a default appearance: an empty badge
in the light tree does not look unit-less and value-less, it shows its placeholders instead.
Fallback content is entirely disabled the moment an assignment exists; the two never mix.

The last section shows that a light-tree change recomputes assignment. Slot elements emit an
event when their assignments change; a component whose computation depends on its content
listens for this event. The Timing and the Rendering Loop lesson's model gives this event's
place in the rendering loop: it is announced after the code that made the change.

## Who Owns Placed Content

Placement does not change a node's tree. The `strong` element assigned to a slot stays in the
document tree; it is only moved to a point in the shadow tree while being painted. Because
ownership does not change, three behaviors follow from this.

**Style comes from the document.** A placed node is styled by the document tree's rule; shadow
tree rules do not generally match it. The component can give placed nodes a limited style: a
special selector targets nodes assigned to a slot, matching only the assigned node
itself — not its children. This limit is deliberate: the component cannot style the internal
structure of content its user gave it, only align its outer box. On conflict the document's
rule wins; the component's style for placed content is only a suggestion.

**Events do not get retargeted.** A placed node's ancestors are in the document tree; an
event's target there is not retargeted, because the event never crossed a shadow boundary. The
component cannot and should not see listeners bound to content in the light tree.

**The accessibility tree derives from the flattened tree.** The structure presented to
assistive technology is the order the user sees. Slots' order in the shadow tree determines
reading order, not the write order in the light tree. When designing a component, slot order
is not a visual preference, it is a decision that carries meaning.

## Summary

- A template element's content is parsed but does not enter the document; it is inert, loads
  no resource, and takes no part in painting.
- Template content is duplicated by deep cloning; the copy carries attributes and ids, not
  listeners or user state.
- The slot assignment rule applies only to the host's direct children; a name matching nothing
  is assigned nowhere and is not rendered.
- The rendered structure is the flattened tree: every slot in the shadow tree replaced by its
  assigned nodes, or by fallback content if there is no assignment.
- A placed node still belongs to the light tree: it is styled by the document's style, its
  events do not cross the shadow boundary, and what style the component can give it is
  limited.

## Next Step

Everything built up to here holds once the page has loaded: the element gets defined, the
shadow tree gets attached, slots get filled. Behind all of it sits a single assumption — the
component's code, template, and data came from the network. When the network connection
weakens or drops entirely, this assumption fails, and the page, however well encapsulated,
lands on a blank error screen. Where the station page is used does not favor this: the person
taking the measurement is usually at the edge of coverage. The next lesson builds a layer that
can meet the page's requests before they reach the network, and that lives independently of the
page.
