Core Concepts

Tesserae builds UIs from C# objects that implement a single interface, configures them with fluent extension methods, and re-renders parts of the tree when observable state changes. This page covers the four ideas you need before reading the component reference:

  • Components — what a Tesserae component is and how it reaches the DOM.
  • The fluent API — how UI factories and extension methods compose.
  • Layout containers — how components are arranged.
  • Reactive state — how a UI stays in sync with changing data.

Components and IComponent

Every Tesserae component implements IComponent, which has one method:

public interface IComponent
{
    HTMLElement Render();
}

Render() returns the underlying DOM element. Containers call it for you when they mount their children, so you rarely call it directly. The exception is the root of your app, which you attach to the page yourself — the same call the live previews on this page end with:

var app = VStack().Children(/* … */);
document.body.appendChild(app.Render());

You create components through the static UI class, which exposes a factory method per component (Button, TextBlock, TextBox, Stack, Grid, …). Add using static Tesserae.UI; and call them without the UI. prefix:

The fluent API

Factories return the concrete component type, and configuration happens through methods that return the same type — so calls chain. Two kinds of methods chain this way:

  • Component methods, defined on the component itself (Button.Primary(), Button.SetIcon(...), TextBlock.SemiBold()).
  • Extension methods, defined on IComponent in IComponentExtensions and applied to any component (sizing, spacing, alignment).
Button("Save")
    .Primary()                 // component method
    .SetIcon(UIcons.Check)     // component method
    .W(120.px())               // extension: fixed width
    .OnClick(() => Save());    // event handler, returns the button

Sizing and spacing helpers

The extension helpers write a CSS property and tag the element so containers can position it correctly (see Layout & Alignment for the wrap-and-transfer details). The ones you reach for most:

Helper Effect
.W(size) / .H(size) Fixed width / height
.MinWidth / .MaxWidth / .MinHeight / .MaxHeight Bounds
.WS() / .HS() / .S() Stretch width / height / both to 100%
.Grow(int = 1) flex-grow on a Stack child, needs a fixed with or min width
.Shrink() / .NoShrink() flex-shrink 1 / 0
.P() / .PT() / .PB() / .PL() / .PR() Padding (all / top / bottom / left / right)
.M() / .MT() / .MB() / .ML() / .MR() Margin

Sizes are UnitSize values produced by numeric extension methods — 16.px(), 50.percent(), 1.fr(), 100.vh().

Capture a reference with `.Var(...)`

A fluent chain returns the component, so you can grab a reference mid-chain with .Var(out var saveButton) and keep configuring. Use it when a later callback needs to read or mutate that same component.

Layout containers

Containers are components that hold other components. Pass children with .Children(...) (or .Add(...) to append one). The common containers:

  • Stack — a flexbox row or column. VStack() stacks vertically, HStack() horizontally.
  • Grid — CSS grid with explicit column and row tracks. Place children with .GridColumn(start, end) / .GridRow(start, end).
  • SplitView / HorizontalSplitView — two resizable panes.
  • Float — an overlay anchored to a corner or edge of its parent.
  • Masonry — variable-height tile columns.

See Layout & Alignment for the full set of alignment and placement options.

Reactive state

A static tree is enough for forms and toolbars, but most UIs need to change after the first render. Tesserae handles this with observables — state holders that notify subscribers when their value changes — and deferred components that re-render in response.

Observables

An IObservable<T> exposes a current Value and lets callers react to changes. The types you use directly:

  • SettableObservable<T> — a single value you read and write through .Value. Setting it notifies subscribers (when the value actually changed).
  • ObservableList<T> — a mutable list (Add, Remove, ReplaceAll, Clear, indexer) that notifies on structural changes.
  • ObservableDictionary<TKey, TValue> / ObservableHashSet<T> — observable collections with the same idea.
var count = new SettableObservable<int>(0);

count.Subscribe(c => Console.WriteLine($"count is now {c}")); // fires immediately with 0
count.Value = 1;                                              // subscriber fires with 1
count.Update(c => c++);                                       // mutate in place, then notify

Subscribe fires immediately with the current value by default; ObserveFutureChanges skips that initial call. Both keep firing on every later change.

Deferred components

Defer (async) and DeferSync (synchronous) create a placeholder that runs a generator function to produce its content. Pass one or more observables and the generator re-runs — replacing the rendered content — whenever any of them changes:

// Re-renders the TextBlock every time `count` changes.
DeferSync(count, c => TextBlock($"Count: {c}"));

This is the core pattern for dynamic UIs: put mutable data in an observable, render the data-dependent part inside a Defer over that observable, and update the observable in event handlers. The example below wires a button's OnClick to increment the observable; the label re-renders on its own.

Two-way binding

Input components such as TextBox, CheckBox, and Toggle implement IBindableComponent<T>. .Bind(observable) connects them to a SettableObservable<T> in both directions: typing updates the observable, and changing the observable in code updates the field. Here a TextBox is bound to a name, and a Defer over the same observable echoes it live:

Rendering collections

For lists that grow and shrink, render an ObservableList<T> inside a Defer. The generator receives the current items, so adding to the list re-renders the view:

For large or frequently-changing lists, prefer the purpose-built collection components — Items List, Virtualized List, and Observable Stack — which diff and recycle rendered rows instead of rebuilding the whole subtree.

Combining and deriving observables

Defer accepts up to ten observables, re-rendering when any of them changes — so a view can depend on several pieces of state at once:

var firstName = new SettableObservable<string>("Ada");
var lastName  = new SettableObservable<string>("Lovelace");

DeferSync(firstName, lastName, (first, last) => TextBlock($"{first} {last}"));

When you need a derived observable rather than a rendered view, the CombinedObservable types merge several observables into one that emits a tuple of their latest values.

Composing reusable UI

Two patterns keep larger UIs manageable:

Factory methods that return IComponent — good for stateless fragments:

public static IComponent LabeledField(string label, IComponent field) =>
    VStack().Children(
        TextBlock(label).Small().SemiBold(),
        field
    );

Component classes that implement IComponent — good when a view owns its own state and event wiring. Build the tree once and return it from Render():

public class SearchPanel : IComponent
{
    private readonly SettableObservable<string> _query = new SettableObservable<string>("");
    private readonly IComponent _content;

    public SearchPanel()
    {
        _content = VStack().Children(
            TextBox().SetPlaceholder("Search…").Bind(_query),
            DeferSync(_query, q => TextBlock(string.IsNullOrEmpty(q)
                ? "Type to search"
                : $"Searching for: {q}"))
        );
    }

    public HTMLElement Render() => _content.Render();
}

Both compose like any other component — pass them to .Children(...) or mount them directly.

Where to go next

© 2026 Curiosity. All rights reserved.