Skip to content

Transitions, Animations & Hover States ​

RTXUI supports CSS-style transitions, keyframe animations and interactive pseudo-classes on DOM elements. This allows you to build terminal applications with smooth color shifts, expanding borders, pulsing indicators and responsive mouse effects without writing manual frame updates.


1. Interactive Pseudo-Classes ​

You can define styles that apply only under specific user interaction states:

  • :hover: Active when the mouse cursor is hovering over the element or its children.
  • :active: Active while the element is being clicked/pressed.
  • :focus: Active when the element has captured keyboard focus.

Example Rulesets ​

css
.button {
  background-color: rgb(30, 58, 138); /* Slate Blue */
}
.button:hover {
  background-color: rgb(29, 78, 216); /* Bright Blue on hover */
}
.button:active {
  background-color: rgb(30, 64, 175); /* Dark Blue on click */
}

Minimal Interactive Demo ​

Below is the interactive tab view for :hover, :active, and :focus pseudo-classes:

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.
//
// The interactive pseudo-classes: :hover, :focus and :active.
//
// Rules can be nested inside their parent with `&`, exactly as in modern CSS,
// so an element's interactive states live next to its base declarations.
//
// Try it: move the mouse over the buttons, Tab between them, and hold the
// mouse button down to see all three states.
#include <rtxui/rtxui.hpp>
#include <string_view>

using namespace rtxui;

class PseudoClassesDemo : public Component<PseudoClassesDemo> {
 public:
  std::string_view view = R"html(
      <div class="card">
        <h1>Pseudo-classes</h1>
        <p>Hover, Tab to focus, or hold down the mouse button.</p>

        <div class="row">
          <div class="btn" tabindex="0">Hover me</div>
          <div class="btn" tabindex="0">Focus me</div>
          <div class="btn" tabindex="0">Press me</div>
        </div>

        <div class="legend">
          <span class="swatch hover"> </span><span>:hover</span>
          <span class="swatch focus"> </span><span>:focus</span>
          <span class="swatch active"> </span><span>:active</span>
        </div>
      </div>

      <style>
        self {
          --bg: rgb(13, 17, 23);
          --border: rgb(48, 54, 61);
          --text: rgb(230, 237, 243);
          --muted: rgb(139, 148, 158);
          --accent: rgb(88, 166, 255);
          --focus: rgb(163, 113, 247);
          --active: rgb(63, 185, 80);

          display: flex;
          align-items: center;
          justify-content: center;
          width: 100%;
          color: var(--text);
        }
        .card {
          border: tall;
          border-color: var(--border);
          background-color: rgb(22, 27, 34);
          padding: 1 3;
        }
        h1 {
          color: var(--accent);
          font-weight: bold;
        }
        p {
          color: var(--muted);
          margin-bottom: 1;
        }
        .row {
          display: flex;
          gap: 2;
          margin-bottom: 1;
        }
        .btn {
          border: tall;
          border-color: var(--border);
          background-color: var(--bg);
          color: var(--text);
          padding: 0 2;
          text-align: center;
          transition: background-color 0.2s ease, border-color 0.2s ease,
                      color 0.2s ease;

          &:hover {
            border-color: var(--accent);
            color: var(--accent);
          }

          &:focus {
            border-color: var(--focus);
            color: var(--focus);
          }

          &:active {
            background-color: var(--active);
            border-color: var(--active);
            color: var(--bg);
          }
        }
        .legend {
          display: flex;
          gap: 1;
          color: var(--muted);
        }
        .swatch {
          width: 2;
        }
        .swatch.hover {
          background-color: var(--accent);
        }
        .swatch.focus {
          background-color: var(--focus);
        }
        .swatch.active {
          background-color: var(--active);
        }
      </style>
    )html";
};

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

2. CSS Transitions ​

The transition property configures how style attributes interpolate smoothly over time instead of changing instantly.

Shorthand Syntax ​

css
transition: <property> <duration> [<timing-function>] [<delay>]

You can declare multiple property transitions by separating them with commas, or use the keyword all to animate all supported variables:

css
transition: background-color 0.2s ease-in-out, width 0.3s ease-out;

Animatable Properties ​

  • Dimensions: width, height (e.g. 20 to 40 cells).
  • Offsets: top, right, bottom, left (or shorthand inset), which move a positioned element. Prefer moving sideways: a terminal has more columns than rows, so a horizontal move takes smaller steps.
  • Translation: translate, which moves any box without moving what is around it. Its percentages are of the box's own size, so from { translate: calc(100% + 2); } slides a box in from just past its own right edge, whatever its width.
  • Borders: border-top-color, border-right-color, border-bottom-color, border-left-color (or shorthand border-color).
  • Colors & Opacities: color, background-color, opacity.
  • Flex Constraints: flex-grow, flex-shrink.

Colors, opacities and translate are the cheapest to animate: they move or resize nothing else, so while they are all that is animating, each frame keeps the last layout and only repaints. Prefer them to sizes and offsets when either would do.

Easing Functions ​

  • linear
  • ease (Default curve)
  • ease-in
  • ease-out
  • ease-in-out
  • Sine curves: ease-in-sine, ease-out-sine, ease-in-out-sine
  • Quad curves: ease-in-quad, ease-out-quad, ease-in-out-quad
  • Cubic curves: ease-in-cubic, ease-out-cubic, ease-in-out-cubic
  • Quart curves: ease-in-quart, ease-out-quart, ease-in-out-quart
  • Quint curves: ease-in-quint, ease-out-quint, ease-in-out-quint
  • Expo curves: ease-in-expo, ease-out-expo, ease-in-out-expo
  • Circ curves: ease-in-circ, ease-out-circ, ease-in-out-circ
  • Back curves: ease-in-back, ease-out-back, ease-in-out-back
  • cubic-bezier(x1, y1, x2, y2) (Defines a custom cubic Bézier easing curve)
  • Steps: steps(n) holds each value for 1/n of the time, then jumps to the next; steps(n, start) jumps at the start of each step instead. step-start and step-end are steps(1, start) and steps(1). Terminal cells are discrete, so steps often look cleaner than a smooth curve.

Minimal Transition Demo ​

Below is the interactive tab view for background-color and border-color transitions:

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.
//
// CSS transitions.
//
// `transition` interpolates a property between its old and new computed value
// whenever a rule stops or starts matching -- here, when :hover applies. Each
// property can carry its own duration and easing function.
//
// Try it: hover each card and watch them settle at different speeds.
#include <rtxui/rtxui.hpp>
#include <string_view>

using namespace rtxui;

class TransitionsDemo : public Component<TransitionsDemo> {
 public:
  std::string_view view = R"html(
      <div class="card">
        <h1>Transitions</h1>
        <p>Hover a tile. Each one eases on a different curve.</p>

        <div class="row">
          <div class="tile linear">
            <span class="name">linear</span>
            <span class="time">0.2s</span>
          </div>
          <div class="tile ease">
            <span class="name">ease-in-out</span>
            <span class="time">0.5s</span>
          </div>
          <div class="tile slow">
            <span class="name">ease-out</span>
            <span class="time">1.0s</span>
          </div>
        </div>
      </div>

      <style>
        self {
          --bg: rgb(13, 17, 23);
          --border: rgb(48, 54, 61);
          --muted: rgb(139, 148, 158);
          --accent: rgb(88, 166, 255);

          display: flex;
          align-items: center;
          justify-content: center;
          width: 100%;
          color: rgb(230, 237, 243);
        }
        .card {
          border: tall;
          border-color: var(--border);
          background-color: rgb(22, 27, 34);
          padding: 1 3;
        }
        h1 {
          color: var(--accent);
          font-weight: bold;
        }
        p {
          color: var(--muted);
          margin-bottom: 1;
        }
        .row {
          display: flex;
          gap: 2;
        }
        .tile {
          display: flex;
          flex-direction: column;
          border: tall;
          border-color: var(--border);
          background-color: var(--bg);
          color: var(--muted);
          padding: 1 2;
          width: 18;
          text-align: center;
        }
        .tile:hover {
          background-color: var(--accent);
          border-color: var(--accent);
          color: var(--bg);
        }
        .name {
          font-weight: bold;
        }
        .linear {
          transition: background-color 0.2s linear, border-color 0.2s linear,
                      color 0.2s linear;
        }
        .ease {
          transition: background-color 0.5s ease-in-out,
                      border-color 0.5s ease-in-out, color 0.5s ease-in-out;
        }
        .slow {
          transition: background-color 1.0s ease-out,
                      border-color 1.0s ease-out, color 1.0s ease-out;
        }
      </style>
    )html";
};

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

3. Keyframe Animations ​

A transition needs a change of state to run. An animation runs on its own as soon as an element declares it, which suits indicators: a pulsing status light, a loading bar, a blinking cursor.

@keyframes names a sequence of styles, and animation plays it:

css
@keyframes pulse {
  from { color: rgb(35, 134, 54); }
  to { color: rgb(126, 231, 135); }
}

.light {
  animation: pulse 0.8s ease-in-out infinite alternate;
}

Shorthand Syntax ​

css
animation: <name> <duration> [<timing-function>] [<delay>] [<iteration-count>]
           [<direction>] [<fill-mode>] [<play-state>]

The parts may come in any order: the first time is the duration and the second the delay. Each also has its own longhand, animation-name, animation-duration, and so on (see the CSS reference).

  • iteration-count: a number, or infinite.
  • direction: normal, reverse, alternate (forwards, then backwards) or alternate-reverse.
  • fill-mode: forwards holds the last keyframe once the animation ends, backwards shows the first one during the delay, both does both. With the default, none, the properties go back to their own values.
  • play-state: paused freezes the animation where it is; running resumes it from there.

Keyframes ​

Blocks are selected by from, to or a percentage, several at once when comma-separated (0%, 100% { ... }). A property missing from the first or the last block animates from, or to, the element's own value. A block can declare its own animation-timing-function, which eases the way from it to the next block:

css
@keyframes blink {
  from { opacity: 1; }
  50% { opacity: 0; }
}
.cursor { animation: blink 1s step-end infinite; }

Keyframes animate the same properties as transitions (listed above). Anything else in a block is reported as a diagnostic, as is an animation whose name matches no @keyframes.

Where Keyframes Are Found ​

Like every other rule, @keyframes belong to the component that declares them: an animation looks for its keyframes in the stylesheet of the component whose rule declared it, and nowhere else. A component can still animate a child component's tag from its own stylesheet, since the tag is part of its own template.

Lifetime ​

  • An animation starts when an element starts declaring it, and keeps running when the component re-renders.
  • Changing its duration, easing or play state does not restart it. Removing it (for example by removing the class that declares it) stops it; adding it back starts it again from the beginning.
  • An element with display: none, or inside one, runs no animation. Its animations start from the beginning once it is displayed, so an element that appears plays them in full, and a hidden one costs no redraws.
  • While any animation plays, the screen keeps redrawing. A headless run waits for finite animations to end, but not for infinite ones.
  • A length animates smoothly between cells, percentages and calc(), in any mix (50% to 10 is calc(25% + 5) halfway). From or to auto, it jumps at the end.

Animation End ​

onanimationend runs a handler once one of the element's animations finishes, as onclick does on a click. It does not run for an infinite animation, nor for one removed before its end. Use it to act after an exit animation, such as removing the item that just faded out:

html
<div class="{row_class}" onanimationend="Removed">{label}</div>

<toast> uses it to hide only once it has slid out.

Keyframes Demo ​

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.
//
// CSS keyframe animations.
//
// `@keyframes` names a sequence of styles, and `animation` plays one on an
// element: for how long, how many times, in which direction, and with which
// easing. Unlike a transition, it needs no change of state to start, which
// suits indicators that move on their own: a pulsing status light, a loading
// bar, a blinking cursor.
//
// Try it: click the button, or Tab to it and press Enter, to pause and resume
// every animation.
#include <rtxui/rtxui.hpp>
#include <string>
#include <string_view>

using namespace rtxui;

class KeyframesDemo : public Component<KeyframesDemo> {
 public:
  bool paused = false;

  std::string state() const { return paused ? "paused" : "running"; }
  std::string label() const { return paused ? "Resume" : "Pause"; }
  void Toggle() { paused = !paused; }

  KeyframesDemo() {
    Bind(paused);
    Bind(state);
    Bind(label);
    Bind(Toggle);
  }

  std::string_view view = R"html(
      <div class="card {state}">
        <h1>Keyframe animations</h1>

        <div class="row">
          <span class="light">●</span>
          <span>Live: pulses forever, alternating direction.</span>
        </div>

        <div class="row">
          <div class="track"><div class="bar"></div></div>
          <span>Loading: steps(10), repeats.</span>
        </div>

        <div class="row">
          <span>Prompt&gt; </span><span class="cursor">█</span>
          <span> Cursor: step-end, blinks.</span>
        </div>

        <div class="row">
          <span class="fade">Faded in once, then held (forwards).</span>
        </div>

        <button onclick="Toggle">{label}</button>
      </div>

      <style>
        @keyframes pulse {
          from {
            color: rgb(35, 134, 54);
          }
          to {
            color: rgb(126, 231, 135);
          }
        }
        @keyframes load {
          from {
            width: 0;
          }
          to {
            width: 20;
          }
        }
        @keyframes blink {
          from {
            opacity: 1;
          }
          50% {
            opacity: 0;
          }
        }
        @keyframes fade-in {
          from {
            opacity: 0;
          }
          to {
            opacity: 1;
          }
        }

        self {
          display: flex;
          align-items: center;
          justify-content: center;
          width: 100%;
          color: rgb(230, 237, 243);
        }
        .card {
          border: tall;
          border-color: rgb(48, 54, 61);
          background-color: rgb(22, 27, 34);
          padding: 1 3;
          width: 60;
        }
        h1 {
          color: rgb(88, 166, 255);
          font-weight: bold;
          margin-bottom: 1;
        }
        .row {
          display: flex;
          margin-bottom: 1;
        }
        .light {
          margin-right: 1;
          animation: pulse 0.8s ease-in-out infinite alternate;
        }
        .track {
          width: 20;
          margin-right: 1;
          background-color: rgb(48, 54, 61);
        }
        .bar {
          width: 0;
          height: 1;
          background-color: rgb(88, 166, 255);
          animation: load 2s steps(10) infinite;
        }
        .cursor {
          animation: blink 1s step-end infinite;
        }
        .fade {
          color: rgb(139, 148, 158);
          animation: fade-in 1s ease-out forwards;
        }
        button {
          border: tall;
          border-color: rgb(48, 54, 61);
          background-color: rgb(13, 17, 23);
          padding: 0 1;
        }
        button:hover, button:focus {
          border-color: rgb(88, 166, 255);
          color: rgb(88, 166, 255);
        }
        .paused .light, .paused .bar, .paused .cursor, .paused .fade {
          animation-play-state: paused;
        }
      </style>
    )html";
};

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

Translate Demo ​

Cards lift through a translate transition when hovered or focused, and each task slides out of its list once done, removed by onanimationend:

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.
//
// Moving boxes with translate.
//
// `translate` moves a box as it is drawn and clicked, after layout, so nothing
// around it moves, and its percentages are of the box's own size. The cards
// lift a cell when hovered or focused, through a transition. Each task slides
// out of its list once done, an animation whose end, `onanimationend`, removes
// it.
//
// Try it: hover or Tab through the cards and click one, then click the tasks,
// or focus one and press Enter.
#include <rtxui/rtxui.hpp>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

using namespace rtxui;

struct Task {
  std::string id;
  std::string name;
  bool leaving = false;
  // Bound state is compared each frame to find what changed.
  bool operator==(const Task&) const = default;
};

// What a template reads of a Task: {task.id}, {task.name} and {task.state}.
void Fields(const Task& task, FieldSink& out) {
  out("id", task.id);
  out("name", task.name);
  out("state", task.leaving ? "leaving" : "");
}

std::vector<Task> AllTasks() {
  return {
      {"1", "Write the release notes"},
      {"2", "Answer the open issues"},
      {"3", "Update the screenshots"},
      {"4", "Tag the release"},
  };
}

class TranslateDemo : public Component<TranslateDemo> {
 public:
  std::string picked = "nothing yet";
  std::vector<Task> tasks = AllTasks();

  void Pick(std::string card) { picked = std::move(card); }

  // Starts the task sliding out; Remove() runs once it is out of sight.
  void Done(std::string id) {
    for (Task& task : tasks) {
      if (task.id == id) {
        task.leaving = true;
      }
    }
  }
  void Remove(std::string id) {
    std::erase_if(tasks, [&](const Task& task) { return task.id == id; });
  }
  void Reset() { tasks = AllTasks(); }
  bool AllDone() const { return tasks.empty(); }

  TranslateDemo() {
    Bind(picked);
    Bind(tasks);
    Bind(Pick);
    Bind(Done);
    Bind(Remove);
    Bind(Reset);
    Bind(AllDone);
  }

  std::string_view view = R"html(
    <div class="page">
      <h1>Translate</h1>

      <p class="label">Cards lift when hovered or focused:</p>
      <div class="cards">
        <div class="tile" tabindex="0" onclick="Pick(Files)">Files</div>
        <div class="tile" tabindex="0" onclick="Pick(Search)">Search</div>
        <div class="tile" tabindex="0" onclick="Pick(Settings)">Settings</div>
      </div>
      <p class="label">Picked: {picked}</p>

      <p class="label">Tasks slide out once done:</p>
      <div class="tasks">
        <for each="{tasks}" as="task" key="{task.id}">
          <div class="task {task.state}" tabindex="0"
               onclick="Done({task.id})" onanimationend="Remove({task.id})">
            ☐ {task.name}
          </div>
        </for>
        <div class="all-done" if="{AllDone}">
          All done. <button onclick="Reset">Reset</button>
        </div>
      </div>
    </div>

    <style>
      self {
        display: flex;
        align-items: center;
        justify-content: center;
        width: 100%;
        color: rgb(230, 237, 243);
      }
      .page {
        border: tall;
        border-color: rgb(48, 54, 61);
        background-color: rgb(22, 27, 34);
        padding: 1 3;
        width: 52;
      }
      h1 {
        color: rgb(88, 166, 255);
      }
      .label {
        color: rgb(139, 148, 158);
      }

      /* Room above the cards for them to lift into. */
      .cards {
        display: flex;
        gap: 2;
        margin-top: 1;
      }
      .tile {
        width: 14;
        padding: 0 1;
        border: tall;
        border-color: rgb(48, 54, 61);
        background-color: rgb(13, 17, 23);
        transition: translate 150ms ease-out, border-color 150ms;
      }
      .tile:hover, .tile:focus {
        translate: 0 -1;
        border-color: rgb(88, 166, 255);
      }

      /* Clipped, so a task leaving goes out of sight at the list's edge. */
      .tasks {
        overflow: hidden;
      }
      .task {
        padding: 0 1;
        background-color: rgb(13, 17, 23);
      }
      .task:focus {
        background-color: rgb(22, 45, 77);
        color: rgb(88, 166, 255);
      }
      @keyframes leave {
        to {
          translate: calc(100% + 2);
        }
      }
      .task.leaving {
        animation: leave 300ms ease-in forwards;
      }
      .all-done {
        color: rgb(63, 185, 80);
      }
    </style>
  )html";
};

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

4. Advanced Transition Examples ​

Primary Animation & Flex Layout Demo ​

Hover over the Hover Me button and click Grow Me to observe interactive transitions reflow the flex container:

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.
//
// Animating layout.
//
// A transition on flex-grow resizes boxes smoothly, and their neighbors make
// room, with no C++ state changing between frames.
#include <rtxui/rtxui.hpp>
#include <string_view>

using namespace rtxui;

class AnimationDemo : public Component<AnimationDemo> {
 public:
  std::string_view view = R"html(
      <div class="card">
        <h1>Animating layout</h1>
        <p>Hover or focus a panel: its flex-grow eases from 1 to 4, and the others make room.</p>

        <div class="row">
          <div class="panel" tabindex="0">Files</div>
          <div class="panel" tabindex="0">Search</div>
          <div class="panel" tabindex="0">Settings</div>
        </div>
      </div>

      <style>
        self {
          --border: rgb(48, 54, 61);
          --muted: rgb(139, 148, 158);
          --accent: rgb(88, 166, 255);
          --accent-background: rgb(44, 83, 128);

          display: flex;
          align-items: center;
          justify-content: center;
          width: 100%;
          color: rgb(230, 237, 243);
        }
        .card {
          border: tall;
          border-color: var(--border);
          background-color: rgb(22, 27, 34);
          padding: 1 3;
          width: 64;
        }
        h1 {
          color: var(--accent);
        }
        p {
          color: var(--muted);
        }
        .row {
          display: flex;
          gap: 1;
        }
        .panel {
          flex-grow: 1;
          flex-basis: 0;
          min-width: 12;
          border: tall;
          border-color: var(--border);
          background-color: rgb(13, 17, 23);
          padding: 1;
          text-align: center;
          transition:
            flex-grow 0.2s ease-in-out, border-color 0.1s linear,
            background-color 0.2s linear;
        }
        .panel:hover, .panel:focus {
          flex-grow: 10;
          border-color: var(--accent);
          color: var(--accent);
          background-color: var(--accent-background);
        }
      </style>
    )html";
};

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

Component-Level Transition Demos ​

  • Slider Transition: Smooth track and thumb focus highlighting. (See Slider Demo)
  • Checkbox Transition: Background check state changes. (See Checkbox Demo)
  • Dropdown Select Menu: Highlight shifts. (See Select Demo)