Advertisement
🦉 Advanced OWL

OWL JS Slots – Content Projection and Layout Composition

Slots allow a parent component to inject its own content into a child component's template. Instead of passing content as a string prop, the parent writes real template markup that the child renders inside a designated t-slot placeholder. This enables powerful layout components — modals, cards, panels, tabs — whose structure is defined by the child but whose content is provided by the caller. Slots are OWL's equivalent of Vue's slots and React's children prop.

⏱️ 18 min read 🎯 Intermediate 📅 Updated 2026 👁️ Lesson 2 of 6

Default Slot

The default slot is the simplest form. The child uses <t t-slot="default"/> as the injection point. The parent writes content between the child's opening and closing tags.

JavaScript – default slot
// ── Child: defines where slot content goes ─────────────────────────
class Card extends Component {
  static template = xml`
    <div class="card">
      <div class="card-body">
        <!-- Parent content is injected here -->
        <t t-slot="default"/>
      </div>
    </div>
  `;
}

// ── Parent: provides slot content ──────────────────────────────────
class App extends Component {
  static components = { Card };

  static template = xml`
    <div>
      <Card>
        <!-- Everything between <Card>...</Card> goes into the default slot -->
        <h2>Welcome!</h2>
        <p>This is the card content.</p>
        <button>Click me</button>
      </Card>
    </div>
  `;
}

Named Slots

Named slots allow multiple injection points in the same component. The child defines multiple t-slot="slotName" points; the parent fills each using t-set-slot="slotName".

JavaScript – named slots (Modal)
// ── Child: Modal with header, body, footer slots ───────────────────
class Modal extends Component {
  static template = xml`
    <div class="modal-overlay" t-on-click.self="() => props.onClose()">
      <div class="modal" role="dialog" t-att-aria-label="props.title">

        <div class="modal-header">
          <!-- Named slot: header -->
          <t t-slot="header">
            <!-- Fallback content if no header slot provided -->
            <h2 t-esc="props.title"/>
          </t>
          <button class="modal-close" t-on-click="() => props.onClose()">×</button>
        </div>

        <div class="modal-body">
          <!-- Default slot: body content -->
          <t t-slot="default"/>
        </div>

        <div class="modal-footer">
          <!-- Named slot: footer -->
          <t t-slot="footer">
            <!-- Fallback: simple close button -->
            <button t-on-click="() => props.onClose()">Close</button>
          </t>
        </div>

      </div>
    </div>
  `;

  static props = {
    title:   { type: String, optional: true },
    onClose: { type: Function },
  };
}

// ── Parent: fills the named slots ──────────────────────────────────
class ConfirmDialog extends Component {
  static components = { Modal };

  static template = xml`
    <Modal onClose.bind="close">

      <!-- Fill the header slot -->
      <t t-set-slot="header">
        <h2 class="text-danger">⚠️ Confirm Delete</h2>
      </t>

      <!-- Default slot (body) -->
      <p>Are you sure you want to delete <strong t-esc="props.itemName"/>?</p>
      <p class="text-muted">This action cannot be undone.</p>

      <!-- Fill the footer slot -->
      <t t-set-slot="footer">
        <button class="btn-secondary" t-on-click="close">Cancel</button>
        <button class="btn-danger"    t-on-click="confirm">Delete</button>
      </t>

    </Modal>
  `;
}
Advertisement

Slot Fallback Content

Content placed inside <t t-slot="name">...fallback...</t> in the child renders only when the parent does not provide content for that slot.

XML – fallback content
<!-- In the child template: -->
<div class="panel">
  <div class="panel-header">
    <t t-slot="title">
      <!-- Shown ONLY when parent does not provide a "title" slot -->
      <span>Untitled Panel</span>
    </t>
  </div>
  <div class="panel-content">
    <t t-slot="default">
      <p class="empty-state">No content provided.</p>
    </t>
  </div>
</div>

Scoped Slots – Data from Child to Parent

Scoped slots let the child pass data back to the parent's slot content. The child provides variables when rendering the slot; the parent can use those variables inside its slot template.

JavaScript – scoped slots
// ── Child: VirtualList — passes each item + index to slot ──────────
class VirtualList extends Component {
  static template = xml`
    <div class="virtual-list">
      <t t-foreach="props.items" t-as="item" t-key="item.id">
        <!-- Slot with scope: passes item and item_index to parent -->
        <t t-slot="item" item="item" index="item_index"/>
      </t>
    </div>
  `;

  static props = {
    items: { type: Array },
  };
}

// ── Parent: uses scoped slot variables ─────────────────────────────
class ProductPage extends Component {
  static components = { VirtualList };

  static template = xml`
    <VirtualList items="state.products">
      <t t-set-slot="item" t-slot-scope="slotProps">
        <!-- slotProps.item and slotProps.index are available here -->
        <div class="product-row">
          <span t-esc="slotProps.index + 1"/>.
          <strong t-esc="slotProps.item.name"/>
          <span>$<t t-esc="slotProps.item.price"/></span>
        </div>
      </t>
    </VirtualList>
  `;
}

Real-World Layout Patterns

JavaScript – Page layout with slots
class PageLayout extends Component {
  static template = xml`
    <div class="page">
      <header class="page-header">
        <t t-slot="header">
          <h1>Page</h1>
        </t>
      </header>

      <div class="page-body">
        <aside t-if="hasLeftPanel" class="page-sidebar">
          <t t-slot="sidebar"/>
        </aside>
        <main class="page-main">
          <t t-slot="default"/>
        </main>
      </div>

      <footer class="page-footer">
        <t t-slot="footer">
          <p>© 2026 ylearner</p>
        </t>
      </footer>
    </div>
  `;

  // Check if parent provided a sidebar slot
  get hasLeftPanel() {
    return !!this.slots.sidebar;
  }
}

Slots: Letting the Parent Inject Content

Props pass data down. Slots pass markup down — they let a parent drop arbitrary template content into a hole the child defines. This is how you build reusable wrappers like cards, modals, and layouts that don't know their contents ahead of time.

// Child: Card defines a slot placeholder
class Card extends Component {
    static template = xml`
        <div class="card">
            <t t-slot="default"/>     <!-- parent content lands here -->
        </div>`;
}

// Parent: everything between the tags fills the default slot
xml`<Card><h2>Title</h2><p>Body text</p></Card>`;

Named slots for multiple holes

// Child template
xml`<div class="modal">
       <header><t t-slot="title"/></header>
       <main><t t-slot="default"/></main>
    </div>`;

// Parent fills each by name
xml`<Modal>
       <t t-set-slot="title">Confirm</t>
       Are you sure?
    </Modal>`;

Why it matters: without slots, a reusable Card would need a prop for every possible piece of content. Slots invert that — the child owns the structure (borders, spacing), the parent owns the content. Clean separation, infinite reuse.

🏋️ Practical Exercise

Build a Tabs component using named slots:

  1. Tabs receives a tabs array prop of { id, label } and renders tab buttons. State: activeId.
  2. For each tab, render a named slot t-slot="[tab.id]" — only show the active tab's slot.
  3. In a parent, provide content for each tab: <t t-set-slot="tab1">...</t>.
  4. Add a scoped slot variant: each tab slot receives an isActive prop from the child so parent content can style itself differently when active.

🔥 Challenge Exercise

Build a reusable Card component that accepts content via a default slot and a named header slot, with fallback content. Explain what slots are (content projection) and how named and scoped slots work.

📋 Summary

  • Default slot: child writes <t t-slot="default"/>; parent provides content between component tags.
  • Named slots: child uses <t t-slot="name"/>; parent fills with <t t-set-slot="name">...</t>.
  • Fallback content: body of <t t-slot="name">fallback</t> renders when parent provides no content for that slot.
  • Scoped slots: child passes props into the slot with <t t-slot="name" key="value"/>; parent accesses them via t-slot-scope="scope" then scope.key.
  • Check if a slot was provided with this.slots.slotName (truthy when content was given).

Interview Questions

  • What are slots in OWL?
  • How do you define and use a slot?
  • What are named slots?
  • What is a scoped slot?
  • How do slots enable reusable layout components?

FAQ

Can slot content access parent state? +

Yes — slot content is evaluated in the parent's scope, not the child's. The template code you write between the child's tags has access to the parent's state, props, and methods. This is the key feature of slots: the child provides structure (CSS, layout), while the parent provides content with full access to its own data.

How is slots different from passing HTML as a prop? +

Props carry data (strings, numbers, objects, functions). Slots carry rendered template fragments — they can include other components, event listeners, reactive expressions, and full OWL template syntax. Passing HTML as a string prop would require t-raw (XSS risk) and loses all reactivity. Slots are the right tool for injecting structured UI.