Skip to content

Component Slots & Composition ​

To build modular, reusable UI layouts, RTXUI supports Component Slots. This allows you to write wrapper components (such as layout grids, panels, cards, or dialogs) that receive and arrange arbitrary child markup passed from their parents.

RTXUI supports both default slots (for simple child wrapping) and named slots (for multi-zone layouts).


Default Slots (<slot>) ​

A default slot acts as a placeholder for any child element nested inside your custom component's tag.

To declare a slot in your component's template, use the <slot></slot> tag:

cpp
class MyPanel : public Component<MyPanel> {
 public:
  std::string_view view = R"html(
    <div class="panel-border">
      <slot></slot>
    </div>
  )html";
};

When using MyPanel inside a parent component, any children you nest inside <MyPanel> will automatically project into the <slot></slot> placeholder:

html
<MyPanel>
  <p>This paragraph is projected inside the panel border.</p>
</MyPanel>

Fallback Content ​

Content placed inside a slot declaration acts as its fallback: it is rendered when the caller projects no elements into that slot, and is replaced whenever projecting content is provided. If the projected content is dynamically unmounted (e.g. by a reactive <if> condition evaluating to false), the slot reverts to displaying its fallback markup:

html
<slot>
  <span>Default placeholder content when no children are provided.</span>
</slot>

Named Slots (<slot.name>) ​

For complex components that have multiple customizable content zones (e.g. a Header, a Body, and a Footer), you can use named slots.

Declaring Named Slots ​

Define slots with dot-separated names inside your component template (e.g. <slot.header> and <slot.footer>):

cpp
class PageLayout : public Component<PageLayout> {
 public:
  std::string_view view = R"html(
    <div class="layout">
      <div class="header-zone">
        <slot.header>Default Header Content</slot.header>
      </div>
      <div class="body-zone">
        <slot></slot> <!-- Default slot for general content -->
      </div>
      <div class="footer-zone">
        <slot.footer></slot.footer>
      </div>
    </div>
  )html";
};

Projecting to Named Slots ​

When consuming a component with named slots, wrap the projected elements inside <template.name> tags:

html
<PageLayout>
  <!-- Projects to <slot.header> -->
  <template.header>
    <h1>Welcome to my App</h1>
  </template.header>

  <!-- Projects to <slot> (default slot) -->
  <p>Here is some page body content...</p>

  <!-- Projects to <slot.footer> -->
  <template.footer>
    <span>Status: Ready</span>
  </template.footer>
</PageLayout>

Selecting Projected Content by Tag (select) ​

<template.name> asks the consumer to say where content goes. Sometimes the component should decide instead, from the tag the consumer wrote. Give the slot a select attribute and it claims projected children with that tag, wherever they appear in the projected content:

html
<!-- Inside the component's own template -->
<div class="legend-line">
  <slot.legend select="legend"></slot.legend>
</div>
<div class="body">
  <slot></slot>
</div>
html
<!-- What the consumer writes: no <template.legend> ceremony -->
<fieldset>
  <legend>Group title</legend>
  <div>body</div>
</fieldset>

The <legend> is routed into the legend slot; everything else falls through to the default slot. This is how the built-in <fieldset> and <details> pick up <legend> and <summary>.

Selection looks through <if>, <elif>, <else> and <for>, so a conditionally rendered <legend> still reaches its slot, and the slot empties again when the condition turns off. Only elements are matched — bare text has no tag and always lands in the default slot.


Interactive Demo ​

Below is the live demo showcasing a reusable Card component using both named slots (for header and footer) and a default slot (for the body content):

Fullscreen Demo
cpp
// Copyright 2026 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
//
// Content projection with <slot>.
//
// A reusable Card component places its caller's markup into named slots, which
// is how every built-in tag is implemented too.
#include <rtxui/rtxui.hpp>

using namespace rtxui;

// A reusable Card component with named header/footer and a default body slot.
class Card : public Component<Card> {
 public:
  std::string_view view = R"html(
    <div class="card-border">
      <div class="card-header">
        <slot.header>Default Header</slot.header>
      </div>
      <div class="card-body">
        <slot></slot>
      </div>
      <div class="card-footer">
        <slot.footer></slot.footer>
      </div>
    </div>
    <style>
      self {
        --bg: rgb(13, 17, 23);

          background-color: var(--bg);
        display: block;
        margin: 1;
      }
      .card-border {
        display: block;
        border: solid;
        border-color: #475569;
        background-color: #0f172a;
      }
      .card-header {
        display: block;
        border-bottom: dashed;
        border-color: #334155;
        padding-left: 1;
        padding-right: 1;
        font-weight: bold;
        color: #38bdf8;
      }
      .card-body {
        display: block;
        padding: 1;
        color: #e2e8f0;
      }
      .card-footer {
        display: block;
        border-top: solid;
        border-color: #334155;
        padding-left: 1;
        padding-right: 1;
        color: #94a3b8;
      }
    </style>
  )html";
};

class SlotsDemo : public Component<SlotsDemo> {
 public:
  std::string_view view = R"html(
    <div class="container">
      <h1>Component Slots & Composition</h1>
      <p>This demo showcases how custom components can define slots for default and named child content.</p>

      <Card>
        <template.header>
          <span>Custom Card Title</span>
        </template.header>

        <div class="content-block">
          <p>This body content is passed into the default slot of the Card component.</p>
          <p>It can contain arbitrary elements, nested components, or text.</p>
        </div>

        <template.footer>
          <span>Footer: Page 1 of 1</span>
        </template.footer>
      </Card>
    </div>
    <style>
      self {
        display: block;
        padding: 1;
        background-color: var(--bg);
        color: white;
        width: 100%;
        height: 100%;
      }
      h1 {
        font-weight: bold;
        color: #38bdf8;
        margin-bottom: 1;
      }
      p {
        color: #94a3b8;
        margin-bottom: 1;
      }
      .content-block {
        display: block;
      }
    </style>
  )html";

  SlotsDemo() { Import<Card>(); }
};

int main() {
  auto app = Ref<SlotsDemo>::New();
  Screen screen(app);
  screen.Loop();
  return 0;
}