Back

Building Flexible Web Components with Slots

Building Flexible Web Components with Slots

Web components are powerful, but passing complex content into them can quickly become messy. Imagine trying to build a card component that needs a header image, title, body text, and action buttons—cramming all that into attributes would create an unreadable mess. That’s where slots come in, transforming how we build flexible, reusable UI components.

This article shows you how to use slots to create web components that accept rich content while maintaining clean, declarative markup. You’ll learn how slots work with shadow DOM, how to implement both default and named slots, and how to style slotted content effectively.

Key Takeaways

  • Slots enable web components to accept complex HTML content instead of cramming everything into attributes
  • Named slots provide precise control over content placement, while default slots handle unspecified content
  • The ::slotted() pseudo-element allows styling of slotted content from within shadow DOM
  • Slots maintain excellent performance by projecting DOM nodes rather than copying them

The Problem: Complex Content in Web Components

Traditional HTML attributes work well for simple values:

<user-avatar src="profile.jpg" size="large"></user-avatar>

But what happens when you need to pass structured content? Consider a card component:

<!-- This gets messy fast -->
<product-card 
  title="Premium Headphones"
  description="<p>High-quality audio with <strong>noise cancellation</strong></p>"
  price="$299"
  button-text="Add to Cart"
  image-src="headphones.jpg">
</product-card>

This approach has several problems:

  • HTML inside attributes requires escaping
  • Complex layouts become impossible to manage
  • The component’s usage doesn’t match standard HTML patterns

How Slots Work: The Basics

Slots let you pass content directly between your component’s tags, just like native HTML elements. Here’s how they transform the previous example:

<product-card>
  <h2 slot="title">Premium Headphones</h2>
  <div slot="description">
    <p>High-quality audio with <strong>noise cancellation</strong></p>
  </div>
  <button slot="action">Add to Cart</button>
</product-card>

Inside your web component, you define where this content appears using the <slot> element:

class ProductCard extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    
    shadow.innerHTML = `
      <style>
        .card {
          border: 1px solid #ddd;
          border-radius: 8px;
          padding: 16px;
        }
      </style>
      <div class="card">
        <slot name="title">Untitled Product</slot>
        <slot name="description">No description available</slot>
        <slot name="action"></slot>
      </div>
    `;
  }
}

customElements.define('product-card', ProductCard);

The content inside each <slot> tag serves as fallback content—it appears when no matching slotted content is provided.

Default vs Named Slots

Web components support two types of slots:

Default Slots

Any content without a slot attribute goes into the default (unnamed) slot:

// Component definition
shadow.innerHTML = `
  <article>
    <h2>Article Title</h2>
    <slot></slot> <!-- Default slot -->
  </article>
`;
<!-- Usage -->
<my-article>
  <p>This paragraph goes into the default slot</p>
  <p>So does this one</p>
</my-article>

Named Slots

Named slots give you precise control over content placement:

// Component definition
shadow.innerHTML = `
  <div class="profile">
    <slot name="avatar"></slot>
    <div class="info">
      <slot name="name">Anonymous</slot>
      <slot name="bio">No bio provided</slot>
    </div>
  </div>
`;
<!-- Usage -->
<user-profile>
  <img slot="avatar" src="jane.jpg" alt="Jane">
  <h3 slot="name">Jane Developer</h3>
  <p slot="bio">Building amazing web components</p>
</user-profile>

Real-World Example: Building a Flexible Card Component

Let’s build a production-ready card component that demonstrates slots in action:

class FlexCard extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    
    shadow.innerHTML = `
      <style>
        :host {
          display: block;
          border: 1px solid #e0e0e0;
          border-radius: 8px;
          overflow: hidden;
          background: white;
        }
        
        .header {
          padding: 16px;
          border-bottom: 1px solid #e0e0e0;
        }
        
        .content {
          padding: 16px;
        }
        
        .footer {
          padding: 16px;
          background: #f5f5f5;
        }
        
        /* Hide empty sections when no slotted content */
        .header:empty {
          display: none;
        }
        
        .footer:empty {
          display: none;
        }
      </style>
      
      <div class="header">
        <slot name="header"></slot>
      </div>
      <div class="content">
        <slot></slot>
      </div>
      <div class="footer">
        <slot name="footer"></slot>
      </div>
    `;
  }
}

customElements.define('flex-card', FlexCard);

Now you can use it with any content structure:

<flex-card>
  <h2 slot="header">Product Details</h2>
  
  <p>Main content goes in the default slot</p>
  <ul>
    <li>Feature 1</li>
    <li>Feature 2</li>
  </ul>
  
  <div slot="footer">
    <button>Buy Now</button>
    <button>Save for Later</button>
  </div>
</flex-card>

Styling Slotted Content

Styling slotted content requires special pseudo-elements:

Using ::slotted()

The ::slotted() pseudo-element targets elements placed into slots:

/* Inside your component's shadow DOM */
::slotted(h2) {
  color: #333;
  margin: 0;
}

::slotted(button) {
  background: #007bff;
  color: white;
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
}

/* Target specific slots */
::slotted([slot="header"]) {
  font-size: 1.2em;
}

Important limitation: ::slotted() only targets the direct slotted element, not its children.

Using :host

The :host pseudo-class styles the component itself:

:host {
  display: block;
  margin: 16px 0;
}

/* Style based on attributes */
:host([variant="primary"]) {
  border-color: #007bff;
}

/* Style based on context */
:host-context(.dark-theme) {
  background: #333;
  color: white;
}

Performance Considerations

Slots are highly performant because they don’t copy DOM nodes—they project them. The slotted content remains in the light DOM but renders as if it’s part of the shadow DOM. This means:

  • Event listeners on slotted content continue to work
  • Styles from the document can still apply (unless blocked by shadow DOM)
  • The browser doesn’t duplicate nodes in memory

Browser Support and Polyfills

Web components slots have excellent browser support in modern browsers. For older browsers, consider using the Web Components polyfills.

Conclusion

Slots transform web components from simple custom elements into powerful, flexible building blocks for reusable UI. By separating structure from content, they enable you to create components that are both highly customizable and easy to use. Whether you’re building a design system or organizing your code better, mastering slots is essential for modern web component development.

Ready to create more flexible web components? Start by refactoring one of your existing components to use slots. Focus on areas where you’re currently passing HTML through attributes or using complex prop structures. Your future self (and your team) will thank you for the cleaner, more maintainable code.

FAQs

Props (attributes) are best for simple values like strings, numbers, or booleans. Slots excel at accepting complex HTML content, multiple elements, or any markup structure. Use props for configuration and slots for content.

Yes, you can modify slotted content at any time since it remains in the light DOM. Simply select the elements with slot attributes and update them like any other DOM elements. The changes reflect immediately in the rendered component.

Slotted content remains in the light DOM, making it fully accessible to search engines and screen readers. This is a major advantage over shadow DOM content, which can be harder for crawlers to index.

All elements with the same slot name appear in that slot in document order. This is useful for creating flexible layouts where users can add multiple items to a single slot area.

No, slots require shadow DOM to function. They're specifically designed to project light DOM content into shadow DOM templates. Without shadow DOM, you'd need to use different content distribution patterns.

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers