---
title: "Build a styled tree in C#"
url: https://recursiveai.org/onerender/cookbook/styled-tree/
description: "Lay out and draw a UI with no HTML at all: build the element tree in C# with CSS property values, render it with LivePage, then change text and hover…"
version: "1.0.0"
---

# Build a styled tree in C#

Lay out and draw a UI with no HTML at all: build the element tree in C# with CSS property values, render it with `LivePage`, then change text and hover state and redraw only what changed.

```csharp
using OneRender;
using OneRender.Image;
using OneRender.Interaction;
using OneRender.Style;
using OneRender.Tree;

var tree = new StyledTree();
StyledNode root = tree.AddRoot("div", new StyleDeclaration()
    .Set("width", "480px").Set("height", "200px").Set("padding", "16px")
    .Set("display", "flex").Set("flex-direction", "column").Set("gap", "12px")
    .Set("background", "#16202c").Set("font", "16px sans-serif").Set("color", "#e8edf3"));

StyledNode title = tree.AddElement(root, "h1", new StyleDeclaration().Set("font-size", "22px").Set("margin", "0"));
tree.AddText(title, "Inventory");

StyledNode bar = tree.AddElement(root, "div", new StyleDeclaration().Set("display", "flex").Set("gap", "8px"));
(StyledNode gold, StyledNode goldText) = AddButton("Gold 120");
(StyledNode gems, _) = AddButton("Gems 4");

var hover = new StyleDeclaration().Set("background", "#3b82f6");
tree.SetStateStyle(gems, ElementStates.Hover, hover);

var page = new LivePage(tree, new LivePageOptions(FontProviders.CreateDefault()) { ViewportWidth = 512, ViewportHeight = 232 });
FlushResult first = page.Flush();
Console.WriteLine($"first flush: {first.Work}, {page.Display.Current.Count} display items");
File.WriteAllBytes("ui-1.png", DisplayListRasterizer.Render(page.Display.Current, 512, 232).ToPng());
page.Display.ClearDamage();

// A frame later: new text and a hover state. Only what changed is restyled, laid out and repainted.
tree.SetText(goldText, "Gold 135");
tree.SetStates(gems, ElementStates.Hover);
FlushResult next = page.Flush();
Console.WriteLine($"second flush: {next.Work}, reused {next.ReusedSubtrees} subtrees, {page.Display.Damage.Count} damaged areas");
for (int i = 0; i < page.Display.Damage.Count; i++) Console.WriteLine($"  damage {page.Display.Damage[i]}");
File.WriteAllBytes("ui-2.png", DisplayListRasterizer.Render(page.Display.Current, 512, 232).ToPng());

(StyledNode Button, StyledNode Text) AddButton(string label)
{
    StyledNode b = tree.AddElement(bar, "div", new StyleDeclaration()
        .Set("padding", "8px 14px").Set("border-radius", "6px").Set("background", "#2a3644"));
    return (b, tree.AddText(b, label));
}
```

```text
first flush: None, 6 display items
second flush: Restyled, BoxesPatched, Relaid, Repainted, reused 2 subtrees, 2 damaged areas
  damage RectF { X = 26, Y = 60, Width = 72.9375, Height = 24, Right = 98.9375, Bottom = 84, IsEmpty = False }
  damage RectF { X = 117, Y = 54, Width = 84, Height = 34, Right = 201, Bottom = 88, IsEmpty = False }
```

`ui-1.png` shows a dark panel with the “Inventory” heading and two buttons. In `ui-2.png` the first button reads “Gold 135” and the second is blue.

## How it works

- **Values are CSS text.** `StyleDeclaration.Set` goes through the same parsers as a style sheet, so a builder tree and an HTML page compute alike, flexbox and grid included. A value the parser rejects is ignored; `TrySet` returns false for it. `var()` isn’t available here.
- **No user-agent style sheet.** Tag names are just names here: an `h1` gets no bold or margin unless you give it some.
- **The constructor does the first layout and paint**, so the first `Flush` has nothing to do (`None`).
- **Edits are logged, not applied.** `SetText`, `SetDeclaration`, `SetProperty`, `SetStates`, `InsertElement`, `Remove` and `Move` record a change. `Flush` then restyles, relays out and repaints only what changed, and the result equals a full rebuild.
- **State styles.** `SetStateStyle` attaches a declaration for `:hover`, `:active` or `:focus`; `SetStates` turns the state on or off.
- **Damage.** `page.Display.Damage` lists the rectangles that changed since `ClearDamage`. Here, only the first button’s text and the second button need redrawing.

## In a game loop

Each frame, apply the game state to the tree and call `Flush`. If `Repainted` is set, draw `page.Display.Current` (or just its damaged areas) and clear the damage. Steady frames, and the usual changes such as a hover, a text change or a transform or opacity animation, allocate nothing. A game engine with its own GPU renderer reads the display list directly: see [Game hosts](https://recursiveai.org/onerender/docs/game-hosts/) and [The display list](https://recursiveai.org/onerender/docs/display-list/).

For HTML pages with input, events and focus instead, see [Hit testing and events](https://recursiveai.org/onerender/cookbook/hit-testing-events/).
