Skip to content

Positioning & Layers ​

By default, RTXUI components arrange elements within the standard flexbox layout flow. For overlay blocks, modals, status bars, or layered widgets, you can use absolute coordinates and z-index ordering.


1. Positioning Modes (position) ​

RTXUI supports element positioning contexts:

  1. static (Default): The element renders in order within the normal flex layout flow.
  2. relative: The element remains in the normal flow but establishes a reference origin point for any descendant child elements set to position: absolute.
  3. absolute: The element is removed from the normal flow. It is positioned relative to its closest parent ancestor that has position: relative (or the root screen boundary if no ancestor is relative).
  4. fixed: The element is removed from the normal flow and positioned relative to the outermost terminal screen viewport.
  5. sticky: The element is positioned based on the user's scroll position. It behaves like relative until the viewport scrolls past a given offset (such as top: 0), at which point it "sticks" to that position, similar to fixed.

2. Offset Coordinates ​

For elements configured with position: absolute, position: fixed, or position: sticky, use these properties to set the spacing offsets from the container margins (measured in character cells):

  • left: Spacing from the container's left edge.
  • right: Spacing from the container's right edge.
  • top: Spacing from the container's top edge.
  • bottom: Spacing from the container's bottom edge.

A sticky element honours all four offsets. top/left pin it as it scrolls up or left out of view; bottom/right pin it while it is still below or to the right of the viewport, which is how a footer stays visible until the content it belongs to scrolls past. When both edges of one axis are set, the leading edge (top, left) wins.

html
<div class="modal-box">Centered Overlay</div>

<style>
  .modal-box {
    position: absolute;
    top: 5;
    left: 10;
    width: 40;
    height: 10;
    border: solid;
  }
</style>

3. Layer Ordering (z-index) ​

To manage depth when multiple absolute or fixed elements overlap, assign the drawing layers using z-index:

  • Elements with a higher z-index are drawn on top of elements with lower values.
  • Default z-index value is 0 (where overlap order falls back to template declaration order).
html
<div class="background-pane">Behind (z-index = 1)</div>
<div class="foreground-pane">In Front (z-index = 10)</div>

<style>
  .background-pane {
    position: absolute;
    z-index: 1;
  }
  .foreground-pane {
    position: absolute;
    z-index: 10;
  }
</style>

Interactive Demo ​

Below is the interactive tab view for layout layering:

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.
//
// position: relative, absolute and fixed, plus z-index stacking.
#include <rtxui/rtxui.hpp>
#include <string>

using namespace rtxui;

class PositioningApp : public Component<PositioningApp> {
 public:
  // State
  int box_x = 10;
  int box_y = 4;
  std::string info_text = "Use buttons to move the absolute red card.";

  // Callback
  void MoveLeft() {
    if (box_x > 1) {
      box_x -= 2;
    }
  }

  void MoveRight() {
    if (box_x < 42) {
      box_x += 2;
    }
  }

  void MoveUp() {
    if (box_y > 1) {
      box_y -= 1;
    }
  }

  void MoveDown() {
    if (box_y < 9) {
      box_y += 1;
    }
  }

  // View
  std::string_view view = R"html(
    <div class="screen-container">
      <div class="header">
        <h1>RTXUI Positioning Demo</h1>
        <p>This demo showcases <strong>absolute</strong>, <strong>fixed</strong> positioning, and <strong>z-index</strong> stacking layers.</p>
      </div>

      <div class="row">
        <!-- Control Panel -->
        <div class="controls">
          <h3>Controls</h3>
          <div class="control-box">
            <div class="control-row">
              <button onclick="MoveLeft">◀ Left</button>
              <button onclick="MoveRight">Right ▶</button>
            </div>
            <div class="control-row">
              <button onclick="MoveUp">▲ Up</button>
              <button onclick="MoveDown">▼ Down</button>
            </div>
          </div>
          <div class="info-panel">
            Position: ({box_x}, {box_y})
          </div>
        </div>

        <!-- Relative Parent Container -->
        <div class="layout-area">
          <!-- Static / normal flow background block -->
          <div class="background-desc">
            This area (blue border) is a relative container. Items inside can be positioned absolutely inside it.
          </div>

          <!-- Midground block with z-index: 5 -->
          <div class="midground-card">
            <span class="card-label">Midground (Z-Index = 5)</span>
          </div>

          <!-- Interactive Absolute block with z-index: 10 -->
          <div class="absolute-card">
            <span class="card-title">Absolute (Z-Index = 10)</span>
            <span class="card-coord">X:{box_x} Y:{box_y}</span>
          </div>
        </div>
      </div>

      <!-- Fixed Status Bar at the bottom of the screen with z-index: 100 -->
      <div class="fixed-status">
        [FIXED STATUS BAR] Current Position: ({box_x}, {box_y}) | RTXUI Layout Engine
      </div>

      <!-- Scrolling filler block to showcase fixed vs absolute positioning -->
      <div class="scroll-filler">
        <h3>Scrolling Demonstration</h3>
        <p>Scroll down to see that the absolute layout area scrolls out of view, while the status bar at the bottom remains fixed in place.</p>
        <p>Scroll down further...</p>
        <p>Line A</p>
        <p>Line B</p>
        <p>Line C</p>
        <p>Line D</p>
        <p>Line E</p>
        <p>Line F</p>
        <p>Line G</p>
        <p>Line H</p>
        <p>Line I</p>
        <p>Line J</p>
      </div>
    </div>

    <style>
      self {
        --surface: rgb(22, 27, 34);
        --border: rgb(48, 54, 61);
        --muted: rgb(139, 148, 158);
        --accent: rgb(88, 166, 255);
        --accent-bright: rgb(121, 192, 255);

        display: block;
        padding: 1 2;
        background-color: rgb(13, 17, 23);
        color: rgb(230, 237, 243);
        width: 100%;
        height: 100%;
        overflow-y: scroll;
      }
      .screen-container {
        display: block;
        width: 100%;
      }
      .header {
        display: block;
        margin-bottom: 2;
      }
      h1 {
        color: var(--accent);
        font-weight: bold;
      }
      h3 {
        color: var(--muted);
        margin-bottom: 1;
      }
      p {
        color: var(--muted);
      }
      .row {
        display: flex;
        flex-direction: row;
        gap: 4;
      }
      .controls {
        display: block;
        border: tall;
        border-color: var(--border);
        padding: 1 2;
        width: 25;
        height: 12;
      }
      .control-box {
        display: flex;
        flex-direction: column;
        gap: 1;
        margin-bottom: 1;
      }
      .control-row {
        display: flex;
        flex-direction: row;
        gap: 1;
      }
      button {
        background-color: var(--surface);
        color: white;
        border: solid;
        border-color: var(--border);
        padding: 0 1;
        text-align: center;
      }
      button:hover {
        background-color: var(--accent);
        border-color: var(--accent-bright);
      }
      .info-panel {
        color: rgb(56, 189, 248);
        font-weight: bold;
      }
      
      /* Positioning Area */
      .layout-area {
        position: relative;
        display: block;
        width: 60;
        height: 15;
        border: solid;
        border-color: var(--accent);
        background-color: var(--surface);
      }
      .background-desc {
        display: block;
        color: var(--muted);
        padding: 1;
      }
      .midground-card {
        position: absolute;
        top: 5;
        left: 20;
        width: 25;
        height: 6;
        background-color: rgb(15, 23, 42);
        border: double;
        border-color: var(--accent-bright);
        z-index: 5;
        padding: 1;
      }
      .card-label {
        color: var(--accent-bright);
      }
      
      /* Target of our absolute moving coordinate state variables */
      .absolute-card {
        position: absolute;
        top: {box_y};
        left: {box_x};
        width: 15;
        height: 4;
        background-color: rgb(220, 38, 38);
        border: double;
        border-color: rgb(248, 81, 73);
        z-index: 10;
        padding: 0 1;
        display: block;
      }
      .card-title {
        color: white;
        font-weight: bold;
        display: block;
      }
      .card-coord {
        color: rgb(254, 205, 211);
        display: block;
      }
      
      /* Fixed layout card */
      .fixed-status {
        position: fixed;
        bottom: 0;
        left: 0;
        width: 100%;
        background-color: rgb(79, 70, 229);
        color: white;
        padding-left: 2;
        z-index: 100;
      }
      .scroll-filler {
        display: block;
        margin-top: 5;
        margin-bottom: 5;
      }
    </style>
  )html";

  // Constructor
  PositioningApp() {
    Bind(box_x);
    Bind(box_y);
    Bind(MoveLeft);
    Bind(MoveRight);
    Bind(MoveUp);
    Bind(MoveDown);
  }
};

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

Sticky Positioning Demo ​

The interactive demo below showcases position: sticky inside a scrollable container. Notice how category headers stay pinned at the top until they are pushed out of the way by the next category.

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.
//
// position: sticky.
//
// Section headers pin to the top of the scroll container while their section is
// on screen.
#include <rtxui/rtxui.hpp>
#include <string>
#include <vector>

using namespace rtxui;

class MonthSection : public Component<MonthSection> {
 public:
  std::string month_index;
  std::string name;
  std::string header_bg = "var(--accent)";
  std::vector<std::string> days;

  std::string_view view = R"html(
    <div class="month-container">
      <div class="sticky-header">{name}</div>
      <for each="{days}" as="day">
        <div class="item">{day}</div>
      </for>
    </div>

    <style>
      self {
        --accent: rgb(88, 166, 255);
        display: block;
      }
      .month-container {
        display: block;
      }
      .sticky-header {
        position: sticky;
        top: 0;
        background-color: {header_bg};
        color: white;
        font-weight: bold;
        padding: 2 1;
        z-index: 10;
      }
      .item {
        display: block;
        padding: 0 2;
        background-color: rgb(30, 41, 59, 0.4);
      }
      .item:hover {
        background-color: rgb(30, 41, 59, 0.8);
      }
    </style>
  )html";

  void set_month_index(std::string value) {
    int idx = std::stoi(value);
    std::vector<std::string> months = {
        "January", "February", "March",     "April",   "May",      "June",
        "July",    "August",   "September", "October", "November", "December",
    };
    std::vector<std::string> colors = {
        "rgb(248, 81, 73)",   // Jan: Red
        "rgb(249, 115, 22)",  // Feb: Orange
        "rgb(245, 158, 11)",  // Mar: Amber
        "rgb(63, 185, 80)",   // Apr: Emerald
        "rgb(20, 184, 166)",  // May: Teal
        "rgb(6, 182, 212)",   // Jun: Cyan
        "rgb(88, 166, 255)",  // Jul: Blue
        "rgb(99, 102, 241)",  // Aug: Indigo
        "rgb(139, 92, 246)",  // Sep: Violet
        "rgb(168, 85, 247)",  // Oct: Purple
        "rgb(236, 72, 153)",  // Nov: Pink
        "rgb(244, 63, 94)"    // Dec: Rose
    };
    std::vector<int> days_in_month = {31, 28, 31, 30, 31, 30,
                                      31, 31, 30, 31, 30, 31};
    if (idx >= 0 && idx < 12) {
      name = months[idx];
      header_bg = colors[idx];
      days.clear();
      for (int d = 1; d <= days_in_month[idx]; ++d) {
        days.push_back("Day " + std::to_string(d));
      }
    }
  }

  // Registers a template variable by hand rather than through Bind(), which
  // is what you drop to when a value needs custom read/write plumbing.
  MonthSection() {
    EnableHotReload();
    auto get_value = [this]() { return month_index; };
    auto set_value = [this](std::string_view val) {
      month_index = std::string(val);
      set_month_index(month_index);
    };
    auto check_and_update = [this, last_val = std::string()]() mutable {
      if (month_index != last_val) {
        last_val = month_index;
        return true;
      }
      return false;
    };
    entries_.push_back({"month-index", get_value, check_and_update, set_value});

    Bind(name);
    Bind(header_bg);
    Bind(days);
  }
};

class StickyDemo : public Component<StickyDemo> {
 public:
  std::vector<std::string> month_indices = {"0", "1", "2", "3", "4",  "5",
                                            "6", "7", "8", "9", "10", "11"};

  std::string_view view = R"html(
    <div class="container">
      <h2>Sticky Calendar Demo</h2>
      <p class="description">
        Use your mouse wheel to scroll the calendar.
        Notice how month names stay pinned at the top until they are pushed out of the way.
      </p>

      <div class="scroll-window">
        <for each="{month_indices}" as="idx">
          <month-section month-index="{idx}" />
        </for>
      </div>
    </div>

    <style>
      self {
        display: block;
        padding: 1 2;
        background-color: rgb(13, 17, 23); /* Deep dark slate background */
        color: rgb(230, 237, 243);
      }
      h2 {
        color: var(--accent); /* Bright blue */
        margin-bottom: 0;
      }
      .description {
        color: rgb(139, 148, 158); /* Muted gray text */
        margin-bottom: 2;
      }
      .scroll-window {
        display: block;
        overflow-y: scroll;
        scroll-speed: 1;
        max-width: 45;
        max-height: 20;
        margin: auto;
      }
    </style>
  )html";

  StickyDemo() {
    Import<MonthSection>("month-section");
    Bind(month_indices);
  }
};

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