Creating a Component

Every Tesserae component is a C# class that produces a DOM element. The contract is a single interface, IComponent:

namespace Tesserae
{
    public interface IComponent
    {
        HTMLElement Render();
    }
}

A parent container (or one of the mount helpers) calls Render() to splice your element into the page. The usual approach is to build the DOM once — in the constructor — keep the root element in a field, and return it from Render().

There are two starting points depending on how much behaviour you need.

Option 1 — implement IComponent directly

Use this for display-only widgets that don't need click/focus/change events. You build the element tree with the DOM helpers from Tesserae.UI and return it.

using static H5.Core.dom;
using static Tesserae.UI;   // Div / Span / I and the _( ) attributes helper

namespace Tesserae
{
    [H5.Name("tss.MyBadge")]
    public class MyBadge : IComponent
    {
        private readonly HTMLElement _inner;

        public MyBadge(string text)
        {
            _inner = Div(_("tss-mybadge"), Span(_(text: text)));
        }

        public HTMLElement Render() => _inner;
    }
}

_( ) builds an Attributes object — _("css-class"), _(text: "hello"), _(id: "x", styles: s => s.color = "red"). The DOM builders (Div, Span, I, DIV(), Raw(html), …) live in UI.HtmlUtil. The [H5.Name("tss.…")] attribute names the generated JavaScript class; it is optional but conventional across the toolkit.

Option 2 — derive from ComponentBase<T, THTML>

Use this when you want events, fluent configuration, margin/padding, and ARIA support out of the box. ComponentBase provides InnerElement, the OnClick/OnChange/OnFocus family, Margin/Padding, and AriaLabel/AriaRole. Set InnerElement in the constructor and return it from Render().

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

namespace Tesserae
{
    [H5.Name("tss.MyToggle")]
    public class MyToggle : ComponentBase<MyToggle, HTMLDivElement>
    {
        public MyToggle()
        {
            InnerElement = Div(_("tss-mytoggle"));
            AttachClick();   // wire up the base class's click plumbing
        }

        public override HTMLElement Render() => InnerElement;

        // Fluent helpers return `this` so calls can be chained.
        public MyToggle On()
        {
            InnerElement.classList.add("tss-on");
            return this;
        }
    }
}

The generic parameters are the component's own type (so fluent methods return the concrete type) and the backing HTML element type.

Sizing, alignment, and containers

The sizing helpers — .Width(), .WS(), .Grow(), .Stretch(), and friends — work on any IComponent. They tag the element with marker attributes; when a container wraps your component in its item <div>, it transfers those styles onto the wrapper (the "wrap-and-transfer" protocol). You don't need to do anything special to support them.

If your component is itself a container that shouldn't be wrapped (for example a Grid nested in a Stack), implement ISpecialCaseStyling and expose a StylingContainer so the helpers write directly onto it.

To accept children, implement IContainer<T, TChild>. In practice most custom components compose existing ones instead — returning a Stack().Children(...) from Render() is often simpler than managing children yourself.

Registering the component

Once the class exists, wire it into the toolkit the same way the built-in components are:

  1. Put the class under Tesserae/src/Components/.

  2. Add a factory method in Tesserae/src/Base/UI.Components.cs:

    public static MyBadge MyBadge(string text) => new MyBadge(text);
    
  3. Add fluent helpers or extension methods under Tesserae/src/Extensions/ if the component needs them.

  4. Add a sample under Tesserae.Tests/ demonstrating it.

Mounting

A top-level component is attached to the page with MountToBody(component) or MountCenteredToBody(component):

private static void Main()
{
    document.body.style.overflow = "hidden";
    MountCenteredToBody(new MyBadge("Ready"));
}

See also

© 2026 Curiosity. All rights reserved.