Skip to content

Slots

This lesson explains Web Components slots and how they let you project light DOM content into a component's internal structure for flexible, composable custom elements.

What Are Slots in Web Components?

Slots are placeholders inside a Web Component where consumers can insert their own markup. They enable composition: the component controls layout and behavior, while the parent page supplies content such as titles, text, icons, and buttons.

Slots are especially useful with Shadow DOM because they let you project light DOM children into an encapsulated internal template without breaking encapsulation.

Concept Description
Slot Element <slot> marks a projection point
Default Slot Receives content without a slot name
Light DOM Markup supplied by the component consumer
Projection Rendering slotted content inside the component
Fallback Content Default markup shown when nothing is slotted
Main Benefit Flexible and composable custom elements

Basic Default Slot Example

class InfoBox extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `
      <section class="info-box">
        <slot>Default info text</slot>
      </section>
    `;
  }
}

customElements.define("info-box", InfoBox);
<info-box>
  This profile was updated 5 minutes ago.
</info-box>

The text inside <info-box> is projected into the default slot and rendered within the component's internal structure.

How Slot Projection Works

Step What Happens
1. Consumer writes markup Content is placed inside the custom element tags
2. Component defines slots <slot> marks where content should appear
3. Browser projects content Slotted nodes render at the slot location
4. DOM ownership stays the same Slotted nodes remain in the logical light DOM tree
5. Fallback appears if empty Default slot content is used when no match exists

Slots with Shadow DOM

class ProfileCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    this.shadowRoot.innerHTML = `
      <style>
        .card {
          padding: 1rem;
          border: 1px solid #dbe5f1;
          border-radius: 0.75rem;
        }
      </style>
      <article class="card">
        <slot></slot>
      </article>
    `;
  }
}

customElements.define("profile-card", ProfileCard);
<profile-card>
  <h2>Alex Chen</h2>
  <p>Frontend Developer</p>
</profile-card>

Shadow DOM keeps styles encapsulated, while slots allow external content to appear in the correct place inside the component.

Fallback Content

class EmptyState extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `
      <section class="empty-state">
        <slot>
          <p>No items to display yet.</p>
        </slot>
      </section>
    `;
  }
}
<!-- Uses fallback content -->
<empty-state></empty-state>

<!-- Replaces fallback content -->
<empty-state>
  <p>No search results found.</p>
</empty-state>

Fallback content gives components sensible defaults while still allowing consumers to override the message when needed.

Projecting Multiple Elements

<content-panel>
  <h2>Recent Activity</h2>
  <p>You signed in from a new device.</p>
  <p>Your password was changed yesterday.</p>
  <button type="button">Review Activity</button>
</content-panel>
class ContentPanel extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `
      <section class="panel">
        <slot></slot>
      </section>
    `;
  }
}

A single default slot can receive multiple child elements. All unmatched light DOM children are projected into that slot in order.

Listening for Slot Changes

class DynamicPanel extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `
      <section>
        <slot></slot>
      </section>
    `;

    this.querySelector("slot").addEventListener(
      "slotchange",
      () => this.handleSlotChange()
    );
  }

  handleSlotChange() {
    const assigned =
      this.querySelector("slot").assignedElements();

    this.classList.toggle(
      "is-empty",
      assigned.length === 0
    );
  }
}

The slotchange event fires when slotted content changes. Use it to update layout, empty states, or accessibility attributes dynamically.

Reading Assigned Slot Content

const slot = this.shadowRoot.querySelector("slot");
const assignedNodes = slot.assignedNodes();
const assignedElements = slot.assignedElements();

assignedElements.forEach((element) => {
  console.log(element.tagName, element.textContent);
});
API Description
slot.assignedNodes() Returns nodes assigned to the slot, including text nodes
slot.assignedElements() Returns only element nodes assigned to the slot
slotchange Event fired when assigned content changes

Styling Slotted Content

this.shadowRoot.innerHTML = `
  <style>
    ::slotted(h2) {
      margin-top: 0;
      font-size: 1.25rem;
    }

    ::slotted(p) {
      color: #475569;
    }

    ::slotted(button) {
      margin-top: 1rem;
    }
  </style>
  <article class="card">
    <slot></slot>
  </article>
`;

Use ::slotted() inside Shadow DOM styles to apply presentation rules to projected content. You cannot style slotted nodes with descendant selectors such as .card h2 from inside the shadow tree.

Composition with Slots

<app-card>
  <h2>Team Updates</h2>
  <p>Design review is scheduled for Monday.</p>
  <app-button>View Calendar</app-button>
</app-card>

Slots encourage composition over configuration. Instead of passing many string props, consumers assemble meaningful markup inside the component.

Slots vs Attributes and Properties

Approach Best For Example
Slots Rich markup and nested content Card body, dialog content, panel footer
Attributes Simple declarative configuration variant="primary"
Properties JavaScript objects and complex values element.items = [...]
Events Output from component to parent item-selected

Light DOM Content and Shadow DOM Layout

Part Owned By Rendered Where
Slotted content Consumer / light DOM Visually inside component via slot
Internal wrapper Component / shadow DOM Inside shadow root
Styles in shadow CSS Component Apply through ::slotted()
Global page styles App / document Can still style slotted light DOM nodes

When to Use Slots

  • Consumers need to pass rich HTML content into a component.
  • You are building layout shells such as cards, panels, and dialogs.
  • You want composable APIs instead of many string-based props.
  • Design system components should accept icons, labels, and actions.
  • You need fallback content when consumers provide nothing.
  • You want to combine Shadow DOM encapsulation with flexible content.

Common Slot Use Cases

  • Card headers, bodies, and footers.
  • Modal titles and dialog content areas.
  • Alert messages and call-to-action buttons.
  • Tabs, accordions, and expandable sections.
  • Layout components such as sidebars and page shells.
  • Empty states and placeholder regions in reusable widgets.

Slot Best Practices

  • Use slots for content projection, not for hidden data storage.
  • Provide meaningful fallback content where it helps UX.
  • Document which slots your component supports.
  • Keep slotted markup semantic for accessibility.
  • Use slotchange when layout depends on assigned content.
  • Style slotted content with ::slotted() inside Shadow DOM.
  • Prefer composition for complex UI instead of large attribute APIs.

Common Slot Mistakes

  • Assuming slotted nodes move permanently into the shadow tree.
  • Trying to query slotted content with normal shadow DOM selectors.
  • Using slots for values that should be attributes or properties.
  • Forgetting fallback content for empty states.
  • Not handling slotchange in dynamic components.
  • Over-styling slotted content and breaking consumer flexibility.
  • Putting non-visual data nodes inside slotted areas unnecessarily.

Key Takeaways

  • Slots let consumers project content into Web Components.
  • The default <slot> receives unmatched child content.
  • Fallback content provides useful defaults when slots are empty.
  • slotchange and assignedElements() help with dynamic behavior.
  • Slots make custom elements composable, flexible, and easier to reuse.

Pro Tip

Design components with slots for the parts that change most often, such as titles, descriptions, icons, and actions. Keep stable structure and behavior inside the component itself.