A live HTML/CSS editor and preview, side by side
A live HTML/CSS editor and preview, side by side.
The preview re-parses on every keystroke using the same template engine that powers every RTXUI app, which is also why this is the docs homepage 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.
//
// A live HTML/CSS editor and preview, side by side.
//
// The preview re-parses on every keystroke using the same template engine that
// powers every RTXUI app, which is also why this is the docs homepage demo.
#include <string>
#include "rtxui/rtxui.hpp"
using namespace rtxui;
namespace {
constexpr std::string_view kDefaultCode = R"html(
<h1>RTXUI Playground</h1>
<p>
Edit the HTML & CSS on the left to see
instant updates. Hover over the button
below to see a CSS transition in action.
</p>
<button>Click me</button>
<button>Click me</button>
<button>Click me</button>
<style>
self {
border: tall;
border-color: rgb(48, 54, 61);
background-color: rgb(22, 27, 34);
color: rgb(230, 237, 243);
padding: 1 2;
}
h1 {
color: rgb(88, 166, 255);
}
p {
color: rgb(139, 148, 158);
}
button {
background-color: rgb(31, 38, 47);
border: tall;
border-color: rgb(48, 54, 61);
color: rgb(230, 237, 243);
padding: 0 2;
transition: all 0.3s ease;
}
button:hover {
background-color: rgb(88, 166, 255);
border-color: rgb(88, 166, 255);
color: rgb(13, 17, 23);
}
button:active {
background-color: rgb(67, 56, 202);
border-color: rgb(67, 56, 202);
color: rgb(230, 237, 243);
transition: all 0s;
}
</style>
)html";
} // namespace
// The right-hand pane. It renders whatever template text `Playground` last
// handed it via `HotReload()` — see `Playground::Digest()` below. Every
// built-in tag a visitor might type has to be `Import`-ed here first, since
// imports are local to the component whose template is being parsed.
class LivePreview : public Component<LivePreview> {
public:
std::string_view view = kDefaultCode;
};
// The left-hand editor shell. It owns the raw template text and, on every
// change, reparses it and forwards it to the `LivePreview` child.
class Playground : public Component<Playground> {
public:
std::string code = std::string(kDefaultCode);
std::string status = "OK";
std::string status_class() const { return status == "OK" ? "ok" : "error"; }
std::string_view view = R"html(
<div class="app">
<div class="pane editor-pane">
<div class="pane-title">Editor
<span class="hint">— HTML + CSS, live</span>
</div>
<textarea
class="editor"
value="{code}"
linenumbers="absolute"
line_wrap="subline"
highlight_current_line="true"
/>
<div class="status {status_class}">{status}</div>
</div>
<div class="pane preview-pane">
<div class="pane-title">Preview</div>
<div class="preview-frame">
<LivePreview id="preview" />
</div>
</div>
</div>
<style>
self {
--surface: rgb(22, 27, 34);
--border: rgb(48, 54, 61);
--muted: rgb(139, 148, 158);
--accent: rgb(88, 166, 255);
display: block;
width: 100%;
height: 100%;
background-color: rgb(15, 23, 42);
color: white;
}
.app {
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
}
.pane {
display: flex;
flex-direction: column;
width: 50%;
height: 100%;
padding: 1;
}
.editor-pane {
border-right: solid;
border-color: var(--border);
}
.pane-title {
font-weight: bold;
color: rgb(129, 140, 248);
margin-bottom: 1;
}
.hint {
font-weight: normal;
color: var(--muted);
}
.editor {
flex-grow: 1;
width: 100%;
padding-left: 1;
padding-right: 1;
overflow-y: scroll;
margin-bottom: 1;
}
/* Themes the gutter/current-line, which live inside <textarea>'s own
template and so aren't reachable by an ordinary .editor .gutter
selector -- see docs/guide/css/basics.md. */
.editor::part(gutter) {
color: var(--border);
}
.editor::part(active) {
color: rgb(129, 140, 248);
}
.editor::part(current-line) {
background-color: rgb(49, 55, 79);
}
.editor::part(selection) {
background-color: rgb(67, 56, 202);
}
.editor::part(cursor) {
color: rgb(230, 237, 243);
}
.editor::part(placeholder) {
color: rgb(100, 116, 139);
}
.status {
color: var(--muted);
}
.status.error {
color: rgb(248, 81, 73);
}
.preview-frame {
flex-grow: 1;
width: 100%;
overflow-y: scroll;
}
</style>
)html";
Playground() {
Bind(code);
Bind(status);
BindComputed(status_class);
Import<LivePreview>();
}
bool Digest() override {
bool changed = Component<Playground>::Digest();
if (code == last_code_) {
return changed;
}
last_code_ = code;
// A parse error reaches the XML error handler installed in main(), which
// overwrites this.
status = "OK";
if (auto* preview =
dynamic_cast<LivePreview*>(QueryComponent("#preview"))) {
preview->HotReload(code);
}
return true;
}
private:
std::string last_code_ = std::string(kDefaultCode);
};
int main() {
auto app = Ref<Playground>::New();
// Without this, a CSS error in the live-edited code (e.g. mid-keystroke,
// before the user finishes typing a rule) would print straight to stderr
// and corrupt the running frame -- Screen owns the terminal in raw mode,
// so that output lands in the middle of the app instead of a scrollback
// the user could read anyway. Route it into the status line instead. XML
// parse errors from HotReload() get the same treatment.
SetCssErrorHandler([app](const CssError& error) {
app->status = "CSS error, line " + std::to_string(error.line + 1) + ": " +
error.message;
});
SetXmlErrorHandler([app](const XmlError& error) {
app->status = "Parse error, line " + std::to_string(error.line + 1) + ": " +
error.message;
});
Screen screen(app);
screen.Loop();
return 0;
}- Source file:
example/playground.cpp - Standalone terminal: ⛶ Open Fullscreen
- Guide: Relevant Documentation
- ← Back to Examples Index

