Curiosity

Tesserae Basics

Tesserae is the UI toolkit Curiosity workspaces (and the desktop app) are built with. You author components as C# classes that implement IComponent; the h5 compiler turns the code into JavaScript that runs in the browser. For the full reference see the Tesserae documentation.

This page is the minimum you need to be productive inside a custom workspace front-end.

A component

Every view implements IComponent. The Render() method returns an HTMLElement.

using H5.Core;
using Tesserae;
using static Tesserae.UI;

public class HelloView : IComponent
{
    public dom.HTMLElement Render() =>
        VStack().Children(
            TextBlock("Hello, workspace").Size(24).SemiBold(),
            Button("Click me").OnClick(() => window.alert("Hi!"))
        ).Render();
}

static Tesserae.UI is the trick that makes the fluent API readable — VStack, HStack, TextBlock, Button are all top-level factories.

Composition

Components are first-class values; pass them around like any other.

IComponent Header(string title) =>
    HStack().AlignItemsCenter().Padding(8.px()).Children(
        Icon(UIcons.Box).Color("#346eeb"),
        TextBlock(title).SemiBold()
    );

IComponent Card(string title, IComponent body) =>
    VStack().Background("white").Border().Padding(12.px()).Children(
        Header(title),
        body
    );

Layout primitives

Primitive Purpose
VStack Vertical flexbox column.
HStack Horizontal flexbox row.
Grid CSS grid with explicit column/row sizing.
Stack Z-axis stacking (use sparingly).
Defer Render an async result without blocking the tree.

Use .Grow(n) / .Width(...) / .Padding(...) etc. for sizing.

Async rendering

Most workspace data comes from endpoints. Don't await in Render() — return a Defer and let Tesserae stream in the result:

public dom.HTMLElement Render() =>
    VStack().Children(
        TextBlock("Top cases").SemiBold(),
        Defer(async () =>
        {
            var cases = await Mosaik.API.Endpoints.CallAsync<CaseList>("top-cases");
            return VStack().Children(cases.Items.Select(c => TextBlock(c.Summary)));
        })
    ).Render();

Defer renders a placeholder while the task runs, then swaps in the result. If you want the placeholder to be a spinner, pass it explicitly: Defer(loader, async () => ...).

Reactive content

For values that change over time, use observables (see State management). Defer understands Observable<T> and re-renders on change.

var query = new Observable<string>("");

VStack().Children(
    TextBox().Placeholder("Search...").Bind(query),
    Defer(query, q => string.IsNullOrEmpty(q)
        ? TextBlock("Type to search.")
        : Defer(async () =>
        {
            var hits = await Mosaik.API.Endpoints.CallAsync<Hit[]>("search", new { q });
            return VStack().Children(hits.Select(h => TextBlock(h.Title)));
        }))
);

Calling endpoints

Mosaik.API.Endpoints.CallAsync<T>(path, body?) is the typed wrapper that handles auth, serialization, and error mapping.

var result = await Mosaik.API.Endpoints.CallAsync<SimilarProductsResponse>(
    "similar-products",
    new { ProductKey = "P-12345", Limit = 3 });

For raw HTTP (rare), use REQ.New("api", "endpoints", "raw").PostJsonAsync(...).

Where to go next

© 2026 Curiosity. All rights reserved.