Rask
Rask
v0.23.1-alpha.0.496
You're reading the Rask v0.23.1-alpha.0.496 guides. Updated .View source
All guides

Elements & the DSL

Rask UI is plain C#: you compose components from a small set of primitives, a generated tag entry for every HTML (and SVG) element, a uniform set of universal props on every tag, and the [...] children indexer to nest them. This guide is the reference for that surface, with a live demo of each piece.

  • Primitives — Text, Raw, Doctype, sibling fragments via [...], and children from strings
  • Tag entries — every HTML element, strongly typed
  • Universal props — Id/Class/Style/Data/Aria on every tag
  • SVG — typed SVG components, no Raw()
  • HTML elements — the full element catalog, by category

Primitives

Three primitives sit beneath every Rask page: Text, Raw, and Doctype. Everything else is built out of them — plus the [...] collection expression, which groups siblings with no wrapping tag.

Text HTML-encodes its value — < and & render as literal characters, never parsed as markup:

PrimitivesTextDemo.cs
namespace Rask.Site.Features;

public sealed partial class PrimitivesTextDemo : Component
{
    protected override Component? Render() => P.Class("mb-0")["1 < 2 && \"safe\""];
}
Live result

1 < 2 && "safe"

It takes its words the way every tag takes children, or as a value — Text["Orders"] and Text.Value("Orders") are the same text node. A component may render nothing but text (Render() => Text[heading.Text]): it has no element of its own, so its words appear, change and go inside whichever element holds it, and a StateHasChanged() patches them there like any other node.

Raw is the escape hatch: verbatim, un-encoded HTML. Use it when you control the source (Markdown output, sanitised snippets) — never on user input.

PrimitivesRawDemo.cs
namespace Rask.Site.Features;

public sealed partial class PrimitivesRawDemo : Component
{
    protected override Component? Render() => P.Class("mb-0")[Raw.Value("Already <strong>safe</strong> HTML")];
}
Live result

Already safe HTML

Security: Raw skips all HTML encoding. Never feed it untrusted strings — sanitize, or use Text.

A bare [...] collection expression returns multiple siblings with no surrounding tag — a Component in its own right, so it's what Render() returns when a component has more than one root — a heading and its paragraph, a layout's header/main/footer:

PrimitivesFragmentDemo.cs
namespace Rask.Site.Features;

public sealed partial class PrimitivesFragmentDemo : Component
{
    protected override Component? Render() => [
        H3.Class("text-lg font-semibold")["A heading"],
        P.Class("mb-0")["A paragraph"]
    ];
}
Live result

A heading

A paragraph

Doctype() emits exactly <!DOCTYPE html> — special-cased, with no attributes, children, or wrapper. An app's pages don't need it (Rask emits the doctype and the rest of the document around the root component — see the document and the HeadAssets override); reach for it when you build a document by hand, for ToHtml() or an email body:

PrimitivesDoctypeDemo.cs
namespace Rask.Site.Features;

public sealed partial class PrimitivesDoctypeDemo : Component
{
    protected override Component? Render() => Span.Class("text-zinc-500 dark:text-white/70")["(emits ", Code["<!DOCTYPE html>"], ")"];
}
Live result
(emits <!DOCTYPE html>)

A bare string is a valid child, so text flows into the [...] indexer alongside elements (it's encoded exactly like Text):

PrimitivesChildrenDemo.cs
namespace Rask.Site.Features;

public sealed partial class PrimitivesChildrenDemo : Component
{
    protected override Component? Render() => Div.Class("mb-0")[
        "plain text, ",
        Strong["bold text, "],
        $"interpolated: {TimeProvider.System.GetLocalNow():yyyy-MM-dd}"
    ];
}
Live result
plain text, bold text, interpolated: 2026-10-22

Tag entries

Every standard HTML element has a generator-emitted chain entry: name it, dot onto it, nest with […]. Tag-specific steps and the universal Id/Class/Style/Data steps sit side by side on every tag.

MDN is the source of truth. The element types, the tags each one renders and every attribute are generated at build time from MDN's own data (@webref/elements, @webref/idl, @mdn/browser-compat-data), kept in src/Rask.Core/Dom/mdn.snapshot.json and refreshed to the latest stable release by the local build. The same snapshot carries UI Events' KeyboardEvent tables (key, code), pinned by commit, from which Keys and Codes are generated: e.Key is Keys.Escape, case Codes.KeyQ: (see keyboard events). The chain you write is named after the tag, and the type behind it after the DOM interface:

You write The type is Why
A.Href("/docs") HTMLAnchorElement MDN's interface for <a>
Em, Section, Nav, Code HTMLElement the DOM gives them no interface of their own
H1 … H6 HTMLHeadingElement one interface, six tags
Td.ColSpan(2), Th.Scope(ThScope.Col) HTMLTableCellElement ColSpan is the IDL colSpan
Input.Bind(() => m.Age) HTMLInputElement<int> the typed control, over MDN's HTMLInputElement

An attribute property is the IDL name in PascalCase (ReadOnly, NoValidate, IsMap, DirName), with two aliases where the DOM only renames for JavaScript's sake: For (htmlFor) and Class (className). Hover any of them for its browser support and links to MDN and the spec. A tag or attribute MDN does not ship in two of Chrome, Firefox and Safari is not offered.

Text & semantic elements:

TagsTextDemo.cs
namespace Rask.Site.Features;

public sealed partial class TagsTextDemo : Component
{
    protected override Component? Render() => Article[
        H1.Class("text-xl font-semibold")["Tags are just methods."],
        P[
            "You can ", Strong["emphasize"], " or ", Em["italicize"],
            " by composing them."
        ],
        Blockquote.Class("border-l-4 border-zinc-200 dark:border-white/10 pl-4 text-base italic text-zinc-500 dark:text-white/70")["A small DSL, an honest day's HTML."]
    ];
}
Live result

Tags are just methods.

You can emphasize or italicize by composing them.

A small DSL, an honest day's HTML.

Form elements:

TagsFormDemo.cs
namespace Rask.Site.Features;

public sealed partial class TagsFormDemo : Component
{
    // The elements below are plain HTML; `Form` binds a model, so this one holds their fields.
    private readonly Fields _fields = new();

    protected override Component? Render() => Form.Model(_fields)[
        Div.Class("mb-2")[
            Ui.Input.Of<string>().Label("Name")
                .Type(InputType.Text)
                .Id("n")
        ],
        Ui.Button.Primary.Submit["Submit"]
    ];

    public sealed class Fields
    {
        public string? Name { get; set; }
    }
}
Live result

Tables:

TagsTableDemo.cs
namespace Rask.Site.Features;

public sealed partial class TagsTableDemo : Component
{
    protected override Component? Render() => Ui.Table[
        Ui.TableColumns[Ui.TableColumn["#"], Ui.TableColumn["Tag"]],
        Ui.TableRows[
            Ui.TableRow[Ui.TableCell["1"], Ui.TableCell[Code["Div"]]],
            Ui.TableRow[Ui.TableCell["2"], Ui.TableCell[Code["Span"]]]
        ]
    ];
}
Live result
#
Tag
1Div
2Span

Media:

TagsMediaDemo.cs
using Rask.Core.Live;

namespace Rask.Site.Features;

public sealed partial class TagsMediaDemo : Component
{
    protected override Component? Render() => Img
        .Src(LiveOptions.PathBase + "/img/rask-placeholder.svg")
        .Alt("Rask")
        .Class("rounded shadow-sm");
}
Live result
Rask

Void elements (Br, Hr, Img, Meta, Link, Input, …) are self-closing, straight from the HTML spec's list, and never accept children:

TagsVoidDemo.cs
namespace Rask.Site.Features;

public sealed partial class TagsVoidDemo : Component
{
    protected override Component? Render() => [
        P.Class("mb-2")["Above the rule"],
        Hr,
        P.Class("mb-0")["Below the rule"]
    ];
}
Live result

Above the rule


Below the rule


Universal props

Every tag accepts Id, Class, Style, Title, Data, the accessibility props Role, TabIndex and Aria, the rest of HTML's global attributes (Lang, Dir, Hidden, Inert, Popover, ContentEditable, Spellcheck, Translate, Draggable), and Attributes — the verbatim escape hatch for anything not named here. They render in a fixed order, ahead of any tag-specific attributes.

Id, Class, Style:

PropsIdClassStyleDemo.cs
namespace Rask.Site.Features;

public sealed partial class PropsIdClassStyleDemo : Component
{
    protected override Component? Render() =>
        Ui.Text
            .Id("card-1")
            .Class("rounded-lg ring-2 ring-accent")
            .Style("padding: 0.6rem 0.8rem;")["Three attributes — id then class then style."];
}
Live result

Three attributes — id then class then style.

Style takes text, or Css — the same declarations written as steps, one per CSS property browsers ship, generated from MDN's data:

Div.Style(Css.Height(ctx.ItemSize.Px))                       // a length: 12.Px, 1.5.Rem, 50.Percent, 100.Vh
Div.Style(Css.Position().Sticky.Top(0.Px).ZIndex(2))         // a keyword from the property's own set
Div.Style(Css.Width("calc(100% - 2rem)"))                    // any text CSS allows
Div.Style(Css.Background(hovering ? "#eef6ff" : null))       // null declares nothing

static readonly Css StickyHead = Css.Position().Sticky.Top(0.Px);
Th.Style(StickyHead.Width(110.Px).TextAlign().Right)["Balance"]

A property's empty call (Css.Display()) offers the keywords that are a whole value on their own — Grid, InlineFlex, None — so a misspelt one does not compile. A value CSS types is taken as that type: a Length or Percentage from the unit literals, a number (Css.Opacity(0.5)), a duration (Css.TransitionDuration(150.Milliseconds)). Everything else a grammar allows — a colour, var(), calc(), a shorthand of several values — is text. A Css is a value: keep one in a field and build on it. Inline style is still for what is only known at run time; the rest belongs in scoped CSS.

Data — expands to data-* attributes; a null value renders as a bare attribute (e.g. data-new), the same way boolean attributes like disabled work. Name the pair directly, or pass several:

Div.Data("rask-no-restore")                                            // bare: data-rask-no-restore
Div.Data("test-id", "primary")
Div.Data(("test-id", "primary"), ("state", "idle"))
Div.Data(new Dictionary<string, string?> { ["test-id"] = "primary" })   // still accepted

The name-only form is the bare attribute, not an empty one — .Data("flag") renders data-flag and .Data("flag", "") renders data-flag="", which are different attributes.

Prefer the pair form over a dictionary. It is not only shorter: a Dictionary for one attribute is three allocations — the dictionary, its bucket array and its entry array — and a chain step re-assigns its property on every render, so that is a per-render cost on every element carrying one. The pair form is a single object the element writer knows by type and writes without materialising an enumerator. Measured over 100 elements each carrying one data-*: 80.7 KB → 63.52 KB, alloc ratio 0.79, and ~11% faster. With three attributes each it is still ahead (87.15 KB → 75.43 KB).

PropsDataDemo.cs
namespace Rask.Site.Features;

public sealed partial class PropsDataDemo : Component
{
    protected override Component? Render() =>
        Div
            .Class("p-2 bg-zinc-50 dark:bg-zinc-900 rounded border")
            .Data(("role", "card"), ("index", "7"), ("new", null))[
            "Inspect the rendered HTML — data-role, data-index, and a bare data-new."];
}
Live result
Inspect the rendered HTML — data-role, data-index, and a bare data-new.

Typed ARIA, Role, TabIndex — every aria-* state and property of the WAI-ARIA spec is a typed step, generated from the spec (.AriaLabel("Close"), .AriaExpanded(open), .AriaLive(AriaLive.Polite)), and AriaRole holds the roles as constants (.Role(AriaRole.Switch)). The Aria bag stays for anything else: it is the data-* model applied to ARIA (each entry expands to aria-{key}, value HTML-encoded, null → bare attribute), takes the same three forms (Span.Aria("label", "Close")), and skips a key a typed step already wrote. See the accessibility guide and the RASK023 img-alt analyzer.

PropsAriaDemo.cs
namespace Rask.Site.Features;

public sealed partial class PropsAriaDemo : Component
{
    // Every aria-* attribute of the WAI-ARIA spec is a typed step, generated from the spec: a bool for true/false,
    // an enum for a keyword. AriaRole holds the roles; the Aria bag stays for anything the steps do not name.
    protected override Component? Render() =>
        Ui.Button.Icon(Ui.IconName.Moon)
            .Role(AriaRole.Switch)
            .AriaChecked(AriaChecked.False)
            .AriaLabel("Toggle dark mode")["Theme"];
}
Live result

The rest of the global attributes

Lang and Dir are the two that matter most, and they are why this section exists: before them, lang was reachable on <html> only — so the page's language worked and a phrase inside it did not. Marking a run of text in another language is [WCAG 3.1.2 Language of Parts][wcag312], and without it a screen reader reads a French quotation with English phonetics:

P["The exhibition is called ", Span.Lang("fr")["Les Demoiselles"], "."]
Span.Dir(Dir.Auto)[userSuppliedName]   // "auto" when you don't know the language at render time

Hidden hides an element from every presentation including assistive technology — prefer it to a display-none class, which hides it visually while leaving it in the accessibility tree. Inert makes a subtree unfocusable and unreachable, which is the correct primitive behind a modal: mark everything outside the dialog inert and focus cannot escape it.

Popover pairs with Button.PopoverTarget for a popover the browser opens, dismisses and focuses with no JavaScript on either side. ContentEditable takes an enum rather than a bool? because PlaintextOnly is the value most editors actually want. Spellcheck and Translate are enumerated rather than bare booleans, so false renders explicitly (translate spells its values yes/no).

Div.Hidden()                                        // hidden
Div.Inert()                                         // inert
Div.Popover(Popover.Auto)                           // popover="auto"
Div.ContentEditable(ContentEditable.PlaintextOnly)  // contenteditable="plaintext-only"
Span.Translate(false)                               // translate="no" — a product name, a username, a code sample

Keyword attributes

An attribute whose value is one of a closed set of keywords takes an enum, generated from the spec's own list of them, so a typo is a compile error rather than an attribute the browser silently ignores:

Img.Src("/hero.png").Alt("Hero").Loading(Loading.Lazy).Decoding(Decoding.Async)
Input.Of<string>().InputMode(InputMode.Numeric).EnterKeyHint(EnterKeyHint.Send)
A.Href("https://example.com").ReferrerPolicy(ReferrerPolicy.NoReferrer)
Th.Scope(ThScope.Col)["Price"]
Button.Type(ButtonType.Submit)["Save"]

The type is named after its step (Loading, CrossOrigin, FetchPriority, Dir, Popover, InputMode), after its tag where only one tag has the attribute or tags disagree on its keywords (ButtonType, ThScope, TrackKind, DialogClosedBy, AreaShape, MetaHttpEquiv), and after the IDL enum the DOM reflects it as where there is one (ReferrerPolicy, ShadowRootMode — the same types the web APIs take). The members are the keywords in PascalCase ("use-credentials" → UseCredentials), and each renders from a switch of literals, so a keyword costs nothing per render. A pair of true/false keywords is a bool? (WritingSuggestions(false)).

An attribute stays a string where its values are not a closed set of words: a MIME type (FormEnctype), a token list (Rel, Sandbox), a browsing context (Target), a set other specs extend (Meta.Name, Autocomplete, Button.Command), or keywords that name no member (Ol.Type's 1/a/A/i/I). A keyword not every browser ships carries its support like any member — hover Popover.Hint — and <RaskBrowserTargets> reports it as RASK098.

Attributes — the escape hatch

Everything the class does not name: microdata (itemscope/itemprop), nonce, part/exportparts, accesskey, slot, inputmode, and anything vendor or experimental. Entries emit verbatim as {key}="{value}" with the value HTML-encoded, and a null value renders the attribute bare, exactly like Data:

Div.Attributes(new() { ["itemscope"] = null, ["itemtype"] = "https://schema.org/Person" })
Div.Attributes(new() { ["inputmode"] = "decimal" })

Prefer a typed property wherever one exists — it is checked, documented and discoverable, and for Hidden/Inert it is also free, where a dictionary is an allocation. lang, dir, hidden, inert, popover, contenteditable, spellcheck and translate all have typed properties now (above), so reach for Attributes for the rest. Nothing here is validated or de-duplicated: naming an attribute a typed property already emits renders it twice and the browser takes the first.

Attributes renders last within the universal block, so a typed property always wins the ordering argument.

PropsAttributesDemo.cs
namespace Rask.Site.Features;

public sealed partial class PropsAttributesDemo : Component
{
    // Two layers, and the order matters. `Lang` is a typed global property, and it is the case that
    // matters most — WCAG 3.1.2 asks for the element that CHANGES language to be marked, so a screen
    // reader switches pronunciation for the quoted phrase and not the whole page.
    //
    // `Attributes` sits underneath as the escape hatch, for what Element names no property for. Prefer
    // the typed property whenever one exists: it is checked, discoverable and documented, and nothing in
    // the bag is validated or de-duplicated. A null value renders the attribute bare.
    protected override Component? Render() =>
        P.Class("mb-0")[
            "The dish arrived with an air of ",
            Span.Class("italic").Lang("fr")["déjà vu"],
            " — and for what has no typed property, a bare ",
            Code.Attributes("data-demo", null)["data-demo"],
            ", written verbatim."];
}
Live result

The dish arrived with an air of déjà vu — and for what has no typed property, a bare data-demo, written verbatim.

What the globals cost

One reference per node, and nothing at all on the static path.

Hidden and Inert are two bits each of the flags byte every component already carries, so they are free. The other six share one reference on the lazy live state — a side object allocated only by an element that actually names one of them — rather than a typed field each, because that state is allocated per node on a mounted page and a field there is paid for by every node of every live session.

Measured against the commit before them: a static render is unchanged (35.31 KB either way), since a plain element keeps its live state null; a live render grows by 8 B per node for the single added reference. Element.WriteAttributes reads the side object once into a local rather than once per attribute, so an element naming no global does less work than a typed-field-each layout would have.

Commands and loading priority

Button.Command and Button.CommandFor generalise what PopoverTarget does for popovers: the button names the element it acts on and the action to invoke, and the browser does the rest with no script on either side. Built-in actions are show-modal, close, request-close, toggle-popover, show-popover and hide-popover; a custom --name dispatches a CommandEvent instead.

Button.Command("show-modal").CommandFor("edit-dialog")["Edit"]
Button.Command("--spin").CommandFor("widget")["Spin"]     // dispatches a CommandEvent
PropsCommandDemo.cs
namespace Rask.Site.Features;

public sealed partial class PropsCommandDemo : Component
{
    // command/commandfor generalise what popovertarget does for popovers: the button names the element
    // it acts on and the action to invoke, and the browser does the rest. There is no OnClick here and
    // no JavaScript anywhere — opening and closing the dialog is entirely declarative.
    //
    // `show-modal` and `close` are two of the built-in actions; a custom `--name` would dispatch a
    // CommandEvent instead of invoking one.
    protected override Component? Render() =>
        Div[
            Ui.Button
                .Command("show-modal")
                .CommandFor("props-command-dialog")["Open the dialog"],
            Dialog.Id("props-command-dialog").Class("p-3 border-0 rounded shadow")[
                P.Class("mb-3")["Opened by ", Code["command"], ", with no handler on either side."],
                Ui.Button.Filled
                    .Command("close")
                    .CommandFor("props-command-dialog")["Close"]]];
}
Live result

Opened by command, with no handler on either side.

FetchPriority (high, low, auto) is on Img, Link, Script and Iframe. The use with a measurable story behind it is high on the LCP image — the browser discovers it at the same moment either way, this moves it ahead of the other images in the queue. Marking everything high marks nothing high.

Blocking on Link and Script takes render, and is the one loading knob that works the other way round: an opt in to blocking rendering until the resource loads, for when a flash of unstyled or un-scripted content is worse than the delay.

ImageSrcset and ImageSizes belong on Link.Rel("preload").As("image"). Without them a responsive image preloads the wrong candidate and the page pays for two downloads, which is the opposite of what preloading it was for.

Img.Src("/hero.png").Alt("Hero").FetchPriority(FetchPriority.High)
Link.Rel("preload").Href("/hero.png").As("image")
    .ImageSrcset("/hero.png 1x, /hero@2x.png 2x").ImageSizes("100vw")

Attribute order is fixed: id, class, style, title, the plain globals (lang, dir, hidden, inert, popover, contenteditable, spellcheck, translate), data-*, role, tabindex, aria-* (typed, then the Aria bag), then Attributes, then tag-specific. Tests enforce it, so the output is predictable for diffing and DOM tooling:

Title is the global title attribute — the browser's hover tooltip. Reach for it where a cell shows an abbreviated value and the exact one belongs behind it (a relative timestamp over the precise instant, a truncated string over its full text). It is not a label: title is invisible to touch users, unreliable with screen readers, and unfocusable, so it may carry supplementary detail but never the only copy of something the reader needs — use Aria for an accessible name.

PropsAttributeOrderDemo.cs
namespace Rask.Site.Features;

public sealed partial class PropsAttributeOrderDemo : Component
{
    protected override Component? Render() =>
        A
            .Href("/tags")
            .Id("out")
            .Class("font-medium text-accent-content underline underline-offset-4")
            .Data("external", "true")["See HTML order"];
}
Live result

SVG

SVG elements are first-class core components, generated from MDN's data like the HTML ones: every SVG element MDN lists as shipping in two browser engines has an entry — svg, g, the shapes, text, gradients, every filter primitive, and the animation elements (Animate, AnimateTransform, Set). They flow through scoped CSS, keyed lists, and event handlers — no Raw() required.

Each entry builds MDN's own type, with MDN's inheritance: Circle is an SVGCircleElement : SVGGeometryElement : SVGGraphicsElement : SVGElement, so PathLength lives once on the geometry base and SystemLanguage on the graphics one. Attributes carry their MDN names (ViewBox, PathLength, StdDeviation) and render in the IDL's order. The entries keep their tag names; the tags HTML also has (a, script, style, title), and path and text (which would shadow System.IO.Path and the Text primitive), take an Svg prefix: SvgA, SvgPath, SvgText.

Presentation attributes (Fill, Stroke, StrokeWidth, StrokeLinecap, MarkerEnd, Cursor, …) live on the shared SVGElement base, so every shape exposes them as optional chain steps. The common ones are fields; the rarer ones cost an element nothing until it sets one:

SvgShapesDemo.cs
namespace Rask.Site.Features;

public sealed partial class SvgShapesDemo : Component
{
    protected override Component? Render() =>
        Svg.Width("200").Height("80").ViewBox("0 0 200 80")[
            Rect.X("5").Y("5").Width("60").Height("70").Rx("8").Fill("#7C3AED"),
            Circle.Cx("105").Cy("40").R("35").Fill("#0D9488"),
            Line
                .X1("150")
                .Y1("10")
                .X2("195")
                .Y2("70")
                .Stroke("#D97706")
                .StrokeWidth("6")
                .StrokeLinecap("round")
        ];
}
Live result

Gradients via <defs> and <linearGradient> (the Rask brand mark itself is built this way); a nested SvgTitle gives the graphic its accessible name:

SvgGradientDemo.cs
namespace Rask.Site.Features;

public sealed partial class SvgGradientDemo : Component
{
    protected override Component? Render() =>
        RaskLogo.Size(120).GradientId("svgPageBolt");
}
Live result
Rask

Clickable shapes — OnClick works on any element; the selection re-renders live over the same transport as the rest of the page:

SvgClickableDemo.cs
using System.Globalization;

namespace Rask.Site.Features;

public sealed partial class SvgClickableDemo : Component
{
    private static readonly (string Name, string Hex)[] Swatches =
    [
        ("Violet", "#7C3AED"),
        ("Indigo", "#512BD4"),
        ("Teal", "#0D9488"),
        ("Amber", "#D97706")
    ];

    private int _selected;

    protected override Component? Render() =>
        [
            Svg.Width("240").Height("48").ViewBox("0 0 240 48")[Swatches.Select(Swatch)],
            P.Class("mt-2 mb-0 text-sm text-zinc-500 dark:text-white/70")[
                "Selected colour: ",
                Strong[Swatches[_selected].Name]
            ]
        ];

    // Keyed so the diff codec reconciles the swatches by identity rather than by position.
    private Component Swatch((string Name, string Hex) swatch, int index) =>
        Circle
            .Cx((24 + (index * 56)).ToString(CultureInfo.InvariantCulture))
            .Cy("24")
            .R("18")
            .Fill(index == _selected ? swatch.Hex : "#e5e7eb")
            .Stroke("#1f2937")
            .StrokeWidth("2")
            .PointerEvents("all")
            .Style("cursor: pointer;")
            .OnClick(() => _selected = index)
            .Key(swatch.Hex);
}
Live result

Selected colour: Violet

Text with <text> and <tspan> — SvgText is the <text> tag (renamed to avoid colliding with the Text primitive); Tspan styles a run inside it:

SvgTextDemo.cs
namespace Rask.Site.Features;

public sealed partial class SvgTextDemo : Component
{
    protected override Component? Render() =>
        Svg.Width("220").Height("60").ViewBox("0 0 220 60")[
            SvgText
                .X("10")
                .Y("38")
                .FontFamily("sans-serif")
                .FontSize("28")
                .FontWeight("bold")
                .Fill("#512BD4")[
                "Ra",
                Tspan.Fill("#0D9488")["sk"]
            ]
        ];
}
Live result
Rask

SVG's own animation elements come from the same data — Animate tweens one attribute, AnimateTransform a transform — and the browser runs them, so nothing re-renders:

SvgAnimateDemo.cs
namespace Rask.Site.Features;

// SVG's own animation elements, straight from MDN's data: the browser runs them, so nothing re-renders.
public sealed partial class SvgAnimateDemo : Component
{
    protected override Component? Render() =>
        Svg.Width("200").Height("80").ViewBox("0 0 200 80")[
            Circle.Cx("40").Cy("40").R("20").Fill("#7C3AED")[
                Animate.AttributeName("r").Values("20;32;20").Dur("2s").RepeatCount("indefinite")
            ],
            Rect.X("120").Y("20").Width("40").Height("40").Rx("6").Fill("#0D9488")[
                AnimateTransform
                    .AttributeName("transform")
                    .Type("rotate")
                    .From("0 140 40")
                    .To("360 140 40")
                    .Dur("3s")
                    .RepeatCount("indefinite")
            ]
        ];
}
Live result

HTML elements

Every standard element is a generated chain entry, composed through the [...] children indexer. The catalog below groups them the way the HTML spec does, and each tag links to its MDN reference.

You rarely need to leave the editor for that reference, though: every element documents itself, from MDN's data, and the documentation is carried onto the chain. Hovering Video names its DOM interface and links the MDN page and the spec, and each step carries its own — so Td's ColSpan says its default and range, and every attribute says which browsers ship it.

Text & inline

[a][a], [abbr][abbr], [b][b], [bdi][bdi], [bdo][bdo], [br][br], [cite][cite], [code][code], [data][data], [dfn][dfn], [del][del], [em][em], [i][i], [ins][ins], [kbd][kbd], [mark][mark], [q][q], [ruby][ruby]/[rp][rp]/[rt][rt], [s][s], [samp][samp], [small][small], [span][span], [strong][strong], [sub][sub], [sup][sup], [time][time], [u][u], [var][var], [wbr][wbr]:

ElementsTextDemo.cs
namespace Rask.Site.Features;

// Every text-level / inline element, live. Each is a chain entry generated from MDN,
// and its children go through the [...] indexer.
public sealed partial class ElementsTextDemo : Component
{
    protected override Component? Render() => Div.Class("flex flex-col gap-2")[
        P[
            "Link ", A.Href("https://example.com").Target("_blank").Rel("noopener")["an anchor"],
            ", ", Strong["strong"], ", ", B["bold"], ", ", Em["emphasis"], ", ", I["idiomatic"],
            ", ", U["underline"], ", ", S["struck"], ", ", Small["small"], ", ", Mark["highlight"],
            ", and ", Span.Class("text-accent-content")["a plain span"], "."
        ],
        P[
            "Inline code ", Code["Div[…]"], ", a key ", Kbd["Ctrl"], "+", Kbd["C"],
            ", sample output ", Samp["exit 0"], ", a variable ", Var["x"], Sub["1"], " to the n", Sup["2"], "."
        ],
        P[
            "Define a term: ", Dfn["Rask"], " is a full-stack C# web framework. Abbreviate a term: ", Abbr["API"],
            ", cite ", Cite["The Pragmatic Programmer"], ", quote ", Q.Cite("https://example.com")["inline quote"],
            ", machine-readable ", Data.Value("42")["forty-two"], ", and a ", Time.DateTime("2026-06-26")["date"], "."
        ],
        // Bidirectional + ruby annotations.
        P[
            "Isolated user text ", Bdi["إعلان"], "; overridden direction ", Bdo.Dir(Dir.Rtl)["this is RTL"], ". ",
            Ruby["漢", Rp["("], Rt["kan"], Rp[")"]], " annotates pronunciation."
        ],
        // A long word with a soft break opportunity, and a line break.
        P.Class("mb-0")[
            "Super", Wbr, "cali", Wbr, "fragilistic.", Br, "Edits: ",
            Ins.Cite("https://example.com").DateTime("2026-06-26")["added"], " and ",
            Del.DateTime("2026-06-25")["removed"], "."
        ]
    ];
}
Live result

Link an anchor, strong, bold, emphasis, idiomatic, underline, struck, small, highlight, and a plain span.

Inline code Div[…], a key Ctrl+C, sample output exit 0, a variable x1 to the n2.

Define a term: Rask is a full-stack C# web framework. Abbreviate a term: API, cite The Pragmatic Programmer, quote inline quote, machine-readable forty-two, and a .

Isolated user text إعلان; overridden direction this is RTL. 漢(kan) annotates pronunciation.

Supercalifragilistic.
Edits: added and removed.

Grouping & lists

[p][p], [hr][hr], [pre][pre], [blockquote][blockquote], [ol][ol]/[ul][ul]/[li][li], [dl][dl]/[dt][dt]/[dd][dd], [figure][figure]/[figcaption][figcaption], [div][div]:

ElementsGroupingDemo.cs
namespace Rask.Site.Features;

// Grouping content + lists: p, hr, pre, blockquote, div, ol/ul/li, dl/dt/dd, figure/figcaption.
public sealed partial class ElementsGroupingDemo : Component
{
    protected override Component? Render() => Div.Class("flex flex-col gap-3")[
        P["A paragraph of flow content, grouped in a ", Code["Div"], "."],
        Pre.Class("bg-zinc-50 dark:bg-zinc-900 border rounded p-2 mb-0")["  preformatted\n  text  keeps   spacing"],
        Blockquote.Class("border-l-4 border-zinc-200 dark:border-white/10 ps-3 text-base italic text-zinc-500 dark:text-white/70").Cite("https://example.com")[
            "A small DSL, an honest day's HTML."],
        Hr,
        Div.Class("grid grid-cols-12 gap-4")[
            Div.Class("col-span-12")[
                P.Class("font-semibold mb-1")["Ordered (start=2, reversed)"],
                Ol.Class("mb-0").Start(2).Reversed()[
                    Li.Value(2)["Second"], Li["First-ish"], Li["Zeroth-ish"]
                ]
            ],
            Div.Class("col-span-12")[
                P.Class("font-semibold mb-1")["Unordered"],
                Ul.Class("mb-0")[Li["Alpha"], Li["Beta"], Li["Gamma"]]
            ],
            Div.Class("col-span-12")[
                P.Class("font-semibold mb-1")["Description"],
                Dl.Class("mb-0")[
                    Dt["Rask"], Dd.Class("mb-1")["A full-stack C# web framework."],
                    Dt["Tag"], Dd.Class("mb-0")["A generated chain entry."]
                ]
            ]
        ],
        Figure.Class("mb-0")[
            Pre.Class("bg-slate-900 text-slate-100 rounded p-2")["Div[Span[\"hi\"]]"],
            Figcaption.Class("mt-2 text-sm text-zinc-500 dark:text-white/70")["Figure: a tiny component tree."]
        ]
    ];
}
Live result

A paragraph of flow content, grouped in a Div.

  preformatted
  text  keeps   spacing
A small DSL, an honest day's HTML.

Ordered (start=2, reversed)

  1. Second
  2. First-ish
  3. Zeroth-ish

Unordered

  • Alpha
  • Beta
  • Gamma

Description

Rask
A full-stack C# web framework.
Tag
A generated chain entry.
Div[Span["hi"]]
Figure: a tiny component tree.

Sections & headings

[h1][h1]–[h6][h6], [header][header], [footer][footer], [main][main], [section][section], [article][article], [aside][aside], [nav][nav], [address][address], [hgroup][hgroup]:

ElementsSectionsDemo.cs
namespace Rask.Site.Features;

// Sectioning + headings: the six headings, hgroup, and the semantic landmarks article/section/nav/
// aside/header/footer/main/address/search.
public sealed partial class ElementsSectionsDemo : Component
{
    protected override Component? Render() => Article.Class("border rounded p-3")[
        Header[
            Hgroup[
                H1.Class("text-xl font-semibold mb-1")["Article title"],
                P.Class("text-zinc-500 dark:text-white/70 mb-0")["A subtitle grouped with the heading"]
            ],
            Nav.Class("text-sm")[
                A.Href("#a").Class("me-2")["Intro"], A.Href("#b")["Details"]
            ]
        ],
        Search.Class("my-2")[
            Input.Of<string>().Type(InputType.Search).Class(ElementsFormsDemo.Field).Placeholder("Search…")
        ],
        Div.Class("grid grid-cols-12 gap-4")[
            Main.Class("col-span-8")[
                Section.Id("a")[H2.Class("text-base font-semibold")["Section heading levels"],
                    P.Class("mb-1")["Headings ", Code["H1"], "–", Code["H6"], ":"],
                    H3.Class("text-base font-semibold mb-0")["H3"], H4.Class("text-base font-semibold mb-0")["H4"],
                    H5.Class("text-base font-semibold mb-0")["H5"], H6.Class("text-base font-semibold mb-0")["H6"]
                ]
            ],
            Aside.Class("col-span-4 text-zinc-500 dark:text-white/70 text-sm")[
                "An ", Code["aside"], " — complementary content."
            ]
        ],
        Footer.Class("border-t pt-2 mt-2 text-sm text-zinc-500 dark:text-white/70")[
            "Footer · ", Address.Class("inline italic")["contact@example.com"]
        ]
    ];
}
Live result

Article title

A subtitle grouped with the heading

Section heading levels

Headings H1–H6:

H3

H4

H5
H6
Footer ·
contact@example.com

Form elements

[form][form], [label][label], [input][input], [button][button], [select][select]/[option][option]/[optgroup][optgroup], [textarea][textarea], [fieldset][fieldset]/[legend][legend], [datalist][datalist], [output][output], [progress][progress], [meter][meter]:

ElementsFormsDemo.cs
namespace Rask.Site.Features;

// Form-associated elements: form, fieldset/legend, label, input, select/optgroup/option, textarea,
// datalist, output, progress, meter, button. (See the Forms page for binding/validation.)
//
// The raw tags ARE the subject here, so they stay raw — drawn with the utilities Flux's input, label and
// button are drawn with, rather than the kit's field components, which the Forms page shows.
public sealed partial class ElementsFormsDemo : Component
{
    // The elements below are plain HTML; `Form` binds a model, so this one holds their fields.
    private readonly Fields _fields = new();

    // Flux's input, select and textarea, as plain utilities on the raw tags.
    internal const string Field =
        "block w-full h-10 rounded-lg border border-zinc-200 border-b-zinc-300/80 bg-white px-3 text-base text-zinc-700 "
        + "shadow-xs placeholder-zinc-400 sm:text-sm dark:border-white/10 dark:bg-white/10 dark:text-zinc-300";

    private const string Area =
        "block w-full rounded-lg border border-zinc-200 border-b-zinc-300/80 bg-white p-3 text-base text-zinc-700 "
        + "shadow-xs placeholder-zinc-400 sm:text-sm dark:border-white/10 dark:bg-white/10 dark:text-zinc-300";

    private const string Caption = "mb-1 inline-block text-sm font-medium text-zinc-800 dark:text-white";

    private const string ResetButton =
        "inline-flex h-10 items-center rounded-lg border border-zinc-200 border-b-zinc-300/80 bg-white px-4 text-sm "
        + "font-medium text-zinc-800 shadow-xs hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-700 dark:text-white "
        + "dark:hover:bg-zinc-600/75";

    protected override Component? Render() => Form.Model(_fields).Class("flex flex-col gap-3")[
        Fieldset.Class("border rounded p-3")[
            Legend.Class("text-base float-none w-auto px-2")["Profile"],
            Div.Class("mb-2")[
                Label.For("nm").Class(Caption)["Name"],
                Input.Of<string>()
                    .Type(InputType.Text)
                    .Id("nm")
                    .Class(Field)
                    .Placeholder("Jane Doe")
                    .List("suggestions"),
                Datalist.Id("suggestions")[Option.Value("Jane Doe"), Option.Value("Ada Lovelace")]
            ],
            Div.Class("mb-2")[
                Label.For("fruit").Class(Caption)["Favourite"],
                Select.Of<string>().Id("fruit").Name("fruit").Class(Field)[
                    Optgroup.Label("Fruit")[Option.Value("apple")["Apple"], Option.Value("pear").Selected()["Pear"]],
                    Optgroup.Label("Veg")[Option.Value("kale")["Kale"]]
                ]
            ],
            Div.Class("mb-0")[
                Label.For("bio").Class(Caption)["Bio"],
                Textarea.Of<string>().Id("bio").Class(Area).Placeholder("About you…")
            ]
        ],
        Div.Class("grid grid-cols-12 gap-4 items-center")[
            Div.Class("col-auto")[
                Label.Class(Caption)["Progress"], Br,
                Progress.Value(0.6).Max(1.0)
            ],
            Div.Class("col-auto")[
                Label.Class(Caption)["Meter"], Br,
                Meter.Value(0.8).Min(0).Max(1).Low(0.2).High(0.9).Optimum(1)
            ],
            Div.Class("col-auto")[
                Label.Class(Caption)["Output"], Br,
                Output.For("fruit")["Pear"]
            ]
        ],
        Div[
            Ui.Button.Primary.Submit["Submit"], " ",
            Button.Type(ButtonType.Reset).Class(ResetButton)["Reset"]
        ]
    ];

    public sealed class Fields
    {
        public string? Name { get; set; }
    }
}
Live result
Profile



Pear

Table elements

[table][table], [caption][caption], [colgroup][colgroup]/[col][col], [thead][thead]/[tbody][tbody]/[tfoot][tfoot], [tr][tr], [th][th]/[td][td]:

ElementsTablesDemo.cs
namespace Rask.Site.Features;

// Tables: table, caption, colgroup/col, thead/tbody/tfoot, tr, th (scope), td (colspan).
public sealed partial class ElementsTablesDemo : Component
{
    protected override Component? Render() => Table.Class("w-full border-collapse text-left text-sm [&_td]:border [&_td]:px-3 [&_td]:py-2 [&_th]:border [&_th]:px-3 [&_th]:py-2")[
        Caption.Class("caption-top")["Quarterly results"],
        Colgroup[Col.Span(1).Class("bg-zinc-50 dark:bg-zinc-900"), Col.Span(2)],
        Thead[
            Tr[Th.Scope(ThScope.Col)["Region"], Th.Scope(ThScope.Col)["Q1"], Th.Scope(ThScope.Col)["Q2"]]
        ],
        Tbody[
            Tr[Th.Scope(ThScope.Row)["North"], Td["10"], Td["12"]],
            Tr[Th.Scope(ThScope.Row)["South"], Td["8"], Td["15"]]
        ],
        Tfoot[
            Tr[Th.Scope(ThScope.Row)["Total"], Td.ColSpan(2).Class("text-right font-bold")["45"]]
        ]
    ];
}
Live result
Quarterly results
RegionQ1Q2
North1012
South815
Total45

Media & embedded

[img][img], [picture][picture]/[source][source], [audio][audio], [video][video]/[track][track], [iframe][iframe], [embed][embed], [object][object], [canvas][canvas], [map][map]/[area][area]:

ElementsMediaDemo.cs
using Rask.Core.Live;

namespace Rask.Site.Features;

// Media & embedded content: img, picture/source, audio, video/track, canvas, iframe (srcdoc — no
// network), embed, object, and an image map (map/area). Self-contained assets so it works offline.
public sealed partial class ElementsMediaDemo : Component
{
    private static string Asset(string name) => LiveOptions.PathBase + "/img/" + name;

    protected override Component? Render() => Div.Class("flex flex-col gap-3")[
        PictureCanvasIframe(),
        EmbedObjectMap(),
        AudioVideo()
    ];

    private static Component PictureCanvasIframe() =>
        Div.Class("flex gap-3 items-start flex-wrap items-center")[
            Figure.Class("m-0")[
                // <picture> picks a <source> by media query, else falls back to <img>.
                Picture[
                    Source.Srcset(Asset("rask-placeholder.svg")).Media("(min-width: 1px)"),
                    Img
                        .Src(Asset("rask-placeholder.svg"))
                        .Alt("Rask logo")
                        .Width(96)
                        .Height(96)
                        .Class("rounded border")
                ],
                Figcaption.Class("mt-2 text-sm text-zinc-500 dark:text-white/70")["picture / source / img"]
            ],
            Figure.Class("m-0")[
                // <canvas> is a JS drawing surface; shown here as the (empty) element.
                Canvas.Width(96).Height(96).Class("border rounded"),
                Figcaption.Class("mt-2 text-sm text-zinc-500 dark:text-white/70")["canvas"]
            ],
            Figure.Class("m-0")[
                Iframe
                    .Srcdoc("<p style='font:13px sans-serif;margin:8px'>An inline iframe document.</p>")
                    .Width("180")
                    .Height("96")
                    .Class("border rounded"),
                Figcaption.Class("mt-2 text-sm text-zinc-500 dark:text-white/70")["iframe (srcdoc)"]
            ]
        ];

    private static Component EmbedObjectMap() =>
        Div.Class("flex gap-3 items-start flex-wrap items-center")[
            Figure.Class("m-0")[
                Embed.Src(Asset("rask-placeholder.svg")).Type("image/svg+xml").Width("96").Height("96"),
                Figcaption.Class("mt-2 text-sm text-zinc-500 dark:text-white/70")["embed"]
            ],
            Figure.Class("m-0")[
                HtmlObject.DataUrl(Asset("rask-placeholder.svg")).Type("image/svg+xml").Width("96").Height("96"),
                Figcaption.Class("mt-2 text-sm text-zinc-500 dark:text-white/70")["object"]
            ],
            Figure.Class("m-0")[
                // <img usemap> + <map>/<area>: a clickable region.
                Img
                    .Src(Asset("rask-placeholder.svg"))
                    .Alt("Map")
                    .Width(96)
                    .Height(96)
                    .UseMap("#regions")
                    .Class("border rounded"),
                Map.Name("regions")[Area.Shape(AreaShape.Rect).Coords("0,0,48,96").Href("#").Alt("left half")],
                Figcaption.Class("mt-2 text-sm text-zinc-500 dark:text-white/70")["img usemap / map / area"]
            ]
        ];

    private static Component AudioVideo() =>
        Div.Class("grid grid-cols-12 gap-4")[
            Div.Class("col-span-12 md:col-span-6")[
                P.Class("text-sm mb-1 text-zinc-500 dark:text-white/70")["audio (controls)"],
                Audio.Controls().Preload(Preload.None).Class("w-full")
            ],
            Div.Class("col-span-12 md:col-span-6")[
                P.Class("text-sm mb-1 text-zinc-500 dark:text-white/70")["video (poster + track)"],
                Video
                    .Controls()
                    .Width(240)
                    .Poster(Asset("rask-placeholder.svg"))
                    .Preload(Preload.None)
                    .Class("border rounded")[
                    Track.Kind(TrackKind.Captions).Src(Asset("captions.vtt")).Srclang("en").Label("English").Default()
                ]
            ]
        ];
}
Live result
Rask logo
picture / source / img
canvas
iframe (srcdoc)
embed
object
Mapleft half
img usemap / map / area

audio (controls)

video (poster + track)

Interactive

[details][details]/[summary][summary], [dialog][dialog], [menu][menu]:

ElementsInteractiveDemo.cs
namespace Rask.Site.Features;

// Interactive elements: details/summary (a native disclosure), dialog (shown inline via Open), and
// menu (a semantic command list).
public sealed partial class ElementsInteractiveDemo : Component
{
    protected override Component? Render() => Div.Class("flex flex-col gap-3")[
        Details.Open().Class("border rounded p-2")[
            Summary.Class("font-semibold")["Disclosure — click to toggle"],
            P.Class("mb-0 mt-2 text-zinc-500 dark:text-white/70")["The browser shows/hides this natively; no JS needed."]
        ],
        // <dialog open> renders in the normal flow (non-modal). showModal() would need JS interop.
        Dialog.Open().Class("position-static block border rounded p-3 m-0 shadow-sm")[
            P.Class("mb-0")["An open ", Code["<dialog open>"], " — non-modal, rendered in flow."]
        ],
        Div[
            P.Class("text-sm mb-1 text-zinc-500 dark:text-white/70")["menu (a semantic toolbar / command list)"],
            Menu.Class("list-inline mb-0")[
                Li.Class("list-inline-item")[Ui.Button["Cut"]],
                Li.Class("list-inline-item")[Ui.Button["Copy"]],
                Li.Class("list-inline-item")[Ui.Button["Paste"]]
            ]
        ]
    ];
}
Live result
Disclosure — click to toggle

The browser shows/hides this natively; no JS needed.

An open <dialog open> — non-modal, rendered in flow.

menu (a semantic toolbar / command list)

  • Document & metadata

    [html][html], [head][head], [body][body], [title][title], [base][base], [link][link], [meta][meta], [style][style], [script][script], [noscript][noscript]:

    ElementsMetadataDemo.cs
    namespace Rask.Site.Features;
    
    // Document & metadata elements — html, head, body, title, base, link, meta, style, script, noscript —
    // build the page shell, so they can't render live *inside* this page. Instead the demo composes a real
    // shell and shows its serialized output via ToHtml(). template/slot (inert/shadow-DOM) render below.
    public sealed partial class ElementsMetadataDemo : Component
    {
        // Illustrative only: a real app declares head content by overriding the `Head` property (which is
        // why RASK019 normally flags Head()[…] children) — this composes the elements directly just to show
        // them and their serialized output, so the analyzer is suppressed here on purpose.
    #pragma warning disable RASK019
        private static Component Shell() => Html.Lang("en").Dir(Dir.Ltr)[
            Head[
                Meta.Charset("utf-8"),
                Meta.Name("viewport").Content("width=device-width, initial-scale=1"),
                Title["My page"],
                Base.Href("/"),
                Link.Rel("stylesheet").Href("/app.css"),
                Style["body{margin:0}"],
                Script.Src("/app.js").Defer(),
                Noscript["This app needs JavaScript."]
            ],
            Body[P["Hello world"]]
        ];
    #pragma warning restore RASK019
    
        protected override Component? Render() => Div.Class("flex flex-col gap-3")[
            Div[
                P.Class("text-sm mb-1 text-zinc-500 dark:text-white/70")[
                    "The structural elements compose a document. Here is a real shell and its serialized HTML:"],
                Pre.Class("bg-slate-900 text-slate-100 rounded p-3 mb-0").Style("white-space:pre-wrap;word-break:break-word")[
                    Code[Shell().ToHtml()]]
            ],
            Div[
                P.Class("text-sm mb-1 text-zinc-500 dark:text-white/70")[
                    "template holds inert content (cloned by JS); slot is a shadow-DOM placeholder:"],
                Div.Class("border rounded p-2")[
                    Template.Id("row-tmpl")[Li["Inert template content"]],
                    Slot.Name("label")["Default slot content"],
                    P.Class("mb-0 mt-1 text-zinc-500 dark:text-white/70 text-sm")[
                        "(the ", Code["template"], " content is hidden by the browser until cloned)"]
                ]
            ]
        ];
    }
    Live result

    The structural elements compose a document. Here is a real shell and its serialized HTML:

    <html lang="en" dir="ltr"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>My page</title><base href="/" /><link href="/app.css" rel="stylesheet" /><style>body{margin:0}</style><script src="/app.js" defer></script><noscript data-r-eb536fcd>This app needs JavaScript.</noscript><!--__rask_head_assets__--></head><body><p data-r-eb536fcd>Hello world</p></body></html>

    template holds inert content (cloned by JS); slot is a shadow-DOM placeholder:

    Default slot content

    (the template content is hidden by the browser until cloned)


    See also: Getting started for building your first component, and Best practices for production patterns.

    Confirm