You're reading the Rask v1.0.0 guides.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.

  • PrimitivesText, Raw, Doctype, sibling fragments via [...], and children from strings
  • Tag entries — every HTML element, strongly typed
  • Universal propsId/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"

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 Head 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-ui-muted")["(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: {DateTime.Today:yyyy-MM-dd}"
    ];
}
Live result
plain text, bold text, interpolated: 2026-09-08

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.

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($"{Tw.Blockquote} text-base")["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")[
            Label.For("n").Class($"{Tw.Label} text-sm mb-1")["Name"],
            Input.Value<string>(null)
                .Type(InputType.Text)
                .Id("n")
                .Class(Tw.Input)
                .Placeholder("Jane Doe")
        ],
        Button.Class(Tw.BtnPrimary).Type("submit")["Submit"]
    ];

    private 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() => Table.Class($"{Tw.Table} text-sm mb-0")[
        Thead[Tr[Th["#"], Th["Tag"]]],
        Tbody[
            Tr[Td["1"], Td[Code["Div"]]],
            Tr[Td["2"], Td[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, …) have SelfClosing => true 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() =>
        Div
            .Id("card-1")
            .Class($"{Tw.Card} ring-violet-500")
            .Style("padding: 0.6rem 0.8rem;")["Three attributes — id then class then style."];
}
Live result
Three attributes — id then class then style.

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-ui-well rounded border")
            .Data(new Dictionary<string, string?> { ["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.

Aria, Role, TabIndexAria is the data-* model applied to ARIA (each entry expands to aria-{key}, value HTML-encoded, null → bare attribute), and takes the same three forms (Span.Aria("label", "Close")); Role and TabIndex are typed because they aren't aria-* attributes. See the accessibility guide and the RASK023 img-alt analyzer.

PropsAriaDemo.cs

namespace Rask.Site.Features;

public sealed partial class PropsAriaDemo : Component
{
    // Role and TabIndex are typed; Aria is a dictionary that expands to aria-* exactly like Data
    // expands to data-* — so the whole ARIA vocabulary is reachable without a property per attribute.
    protected override Component? Render() =>
        Button
            .Class(Tw.BtnOutlinePrimary)
            .Role("switch")
            .TabIndex(0)
            .Aria(new Dictionary<string, string?> { ["label"] = "Toggle dark mode", ["pressed"] = "false" })[
            UiIcon.Name(UiIconName.Moon).Class("me-1"),
            "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("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 is a string rather than a bool? because "plaintext-only" 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(true)                     // hidden
Div.Inert(true)                      // inert
Div.Popover("auto")                  // popover="auto"
Div.ContentEditable("plaintext-only")
Span.Translate(false)                // translate="no" — a product name, a username, a code sample

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(new Dictionary<string, string?> { ["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[
            Button
                .Class(Tw.BtnOutlinePrimary)
                .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."],
                Button
                    .Class(Tw.BtnSecondary)
                    .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("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-*, 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("link link-primary")
            .Data(new Dictionary<string, string?> { ["external"] = "true" })["See HTML order"];
}
Live result

SVG

SVG elements are first-class core components. svg, g, path, the shapes, text, gradients and filters all have typed entries that flow through scoped CSS, keyed lists, and event handlers — no Raw() required.

Shapes inside an <svg> — presentation attributes (Fill, Stroke, StrokeWidth, StrokeLinecap, …) live on the shared SvgElement base, so every shape exposes them as optional chain steps:

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

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")[BuildSwatches()],
            P.Class("mt-2 mb-0 text-sm text-ui-muted")[
                "Selected colour: ",
                Strong[Swatches[_selected].Name]
            ]
        ];

    // Keyed so the diff codec reconciles the swatches by identity rather than by position.
    private List<Component> BuildSwatches()
    {
        var children = new List<Component>();
        for (var i = 0; i < Swatches.Length; i++)
        {
            var index = i;
            var (_, hex) = Swatches[i];
            children.Add(Circle
                .Cx((24 + (i * 56)).ToString())
                .Cy("24")
                .R("18")
                .Fill(i == _selected ? hex : "#e5e7eb")
                .Stroke("#1f2937")
                .StrokeWidth("2")
                .PointerEvents("all")
                .Style("cursor: pointer;")
                .OnClick(() => _selected = index)
                .Key(hex));
        }

        return children;
    }
}
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

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 component documents itself, and the documentation is carried onto the chain. Hovering Video says what <video> is and links the same MDN page, and each step carries its own description — so Meter's Low/High/Optimum, Track's Kind, and Iframe's Sandbox explain themselves at the call site rather than sending you to a search engine.

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 generator-emitted factory in
// Rask.Core.Components.Generated; 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")["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 C# UI framework. Abbreviate it ", Abbr["UI"],
            ", 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("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 C# UI framework. Abbreviate it UI, 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-ui-well border rounded p-2 mb-0")["  preformatted\n  text  keeps   spacing"],
        Blockquote.Class($"{Tw.Blockquote} text-base border-l ps-3").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(true)[
                    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 C# UI framework."],
                    Dt["Tag"], Dd.Class("mb-0")["A generated factory method."]
                ]
            ]
        ],
        Figure.Class("mb-0")[
            Pre.Class("bg-slate-900 text-slate-100 rounded p-2")["Div()[Span()[\"hi\"]]"],
            Figcaption.Class(Tw.FigureCaption)["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 C# UI framework.
Tag
A generated factory method.
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-ui-muted 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.Value<string>(null).Type(InputType.Search).Class(Tw.Input).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-ui-muted text-sm")[
                "An ", Code["aside"], " — complementary content."
            ]
        ],
        Footer.Class("border-t pt-2 mt-2 text-sm text-ui-muted")[
            "Footer · ", Address.Class("inline italic")["contact@example.com"]
        ]
    ];
}
Live result

Article title

A subtitle grouped with the heading

Section heading levels

Headings H1H6:

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.)
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();

    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($"{Tw.Label} text-sm mb-1")["Name"],
                Input.Value<string>(null)
                    .Type(InputType.Text)
                    .Id("nm")
                    .Class(Tw.Input)
                    .Placeholder("Jane Doe")
                    .List("suggestions"),
                Datalist.Id("suggestions")[Option.Value("Jane Doe"), Option.Value("Ada Lovelace")]
            ],
            Div.Class("mb-2")[
                Label.For("fruit").Class($"{Tw.Label} text-sm mb-1")["Favourite"],
                Select.Value<string>(null).Id("fruit").Name("fruit").Class(Tw.Select)[
                    Optgroup.Label("Fruit")[Option.Value("apple")["Apple"], Option.Value("pear").Selected(true)["Pear"]],
                    Optgroup.Label("Veg")[Option.Value("kale")["Kale"]]
                ]
            ],
            Div.Class("mb-0")[
                Label.For("bio").Class($"{Tw.Label} text-sm mb-1")["Bio"],
                Textarea.Value<string>(null).Id("bio").Class(Tw.Input).Placeholder("About you…")
            ]
        ],
        Div.Class("grid grid-cols-12 gap-4 items-center")[
            Div.Class("col-auto")[
                Label.Class($"{Tw.Label} text-sm mb-1")["Progress"], Br,
                Progress.Value(0.6).Max(1.0)
            ],
            Div.Class("col-auto")[
                Label.Class($"{Tw.Label} text-sm mb-1")["Meter"], Br,
                Meter.Value(0.8).Min(0).Max(1).Low(0.2).High(0.9).Optimum(1)
            ],
            Div.Class("col-auto")[
                Label.Class($"{Tw.Label} text-sm mb-1")["Output"], Br,
                Output.For("fruit")["Pear"]
            ]
        ],
        Div[
            Button.Class(Tw.BtnPrimary).Type("submit")["Submit"], " ",
            Button.Class(Tw.BtnOutlineSecondary).Type("reset")["Reset"]
        ]
    ];

    private 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($"{Tw.Table} text-sm [&_td]:border [&_th]:border mb-0")[
        Caption.Class("caption-top")["Quarterly results"],
        Colgroup[Col.Span(1).Class("bg-ui-well"), Col.Span(2)],
        Thead[
            Tr[Th.Scope("col")["Region"], Th.Scope("col")["Q1"], Th.Scope("col")["Q2"]]
        ],
        Tbody[
            Tr[Th.Scope("row")["North"], Td["10"], Td["12"]],
            Tr[Th.Scope("row")["South"], Td["8"], Td["15"]]
        ],
        Tfoot[
            Tr[Th.Scope("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")[
        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(Tw.FigureCaption)["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(Tw.FigureCaption)["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(Tw.FigureCaption)["iframe (srcdoc)"]
            ]
        ],
        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(Tw.FigureCaption)["embed"]
            ],
            Figure.Class("m-0")[
                HtmlObject.DataUrl(Asset("rask-placeholder.svg")).Type("image/svg+xml").Width(96).Height(96),
                Figcaption.Class(Tw.FigureCaption)["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("rect").Coords("0,0,48,96").Href("#").Alt("left half")],
                Figcaption.Class(Tw.FigureCaption)["img usemap / map / area"]
            ]
        ],
        Div.Class("grid grid-cols-12 gap-4")[
            Div.Class("md:col-span-6")[
                P.Class("text-sm mb-1 text-ui-muted")["audio (controls)"],
                Audio.Controls(true).Preload("none").Class("w-full")
            ],
            Div.Class("md:col-span-6")[
                P.Class("text-sm mb-1 text-ui-muted")["video (poster + track)"],
                Video
                    .Controls(true)
                    .Width(240)
                    .Poster(Asset("rask-placeholder.svg"))
                    .Preload("none")
                    .Class("border rounded")[
                    Track.Kind("captions").Src(Asset("captions.vtt")).Srclang("en").Label("English").Default(true)
                ]
            ]
        ]
    ];
}
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(true).Class("border rounded p-2")[
            Summary.Class("font-semibold")["Disclosure — click to toggle"],
            P.Class("mb-0 mt-2 text-ui-muted")["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(true).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-ui-muted")["menu (a semantic toolbar / command list)"],
            Menu.Class("list-inline mb-0")[
                Li.Class("list-inline-item")[Button.Type("button").Class(Tw.BtnOutlineSecondary)["Cut"]],
                Li.Class("list-inline-item")[Button.Type("button").Class(Tw.BtnOutlineSecondary)["Copy"]],
                Li.Class("list-inline-item")[Button.Type("button").Class(Tw.BtnOutlineSecondary)["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("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(true),
                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-ui-muted")[
                    "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-ui-muted")[
                    "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-ui-muted 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>This app needs JavaScript.</noscript><!--__rask_head_assets__--></head><body><p>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.