Tags are just methods.
You can emphasize or italicize by composing them.
A small DSL, an honest day's HTML.
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.
Text, Raw, Doctype, sibling fragments via [...], and children from stringsId/Class/Style/Data/Aria on every tagRaw()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:
namespace Rask.Site.Features;
public sealed partial class PrimitivesTextDemo : Component
{
protected override Component? Render() => P.Class("mb-0")["1 < 2 && \"safe\""];
}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.
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")];
}Already safe HTML
Security:
Rawskips all HTML encoding. Never feed it untrusted strings — sanitize, or useText.
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:
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"]
];
}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:
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>"], ")"];
}<!DOCTYPE html>)A bare string is a valid child, so text flows into the [...] indexer alongside elements (it's
encoded exactly like Text):
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}"
];
}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:
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."]
];
}You can emphasize or italicize by composing them.
A small DSL, an honest day's HTML.
Form elements:
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; }
}
}Tables:
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"]]]
]
];
}# | Tag |
|---|---|
| 1 | Div |
| 2 | Span |
Media:
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");
}Void elements (Br, Hr, Img, Meta, Link, Input, …) are self-closing, straight from the HTML
spec's list, and never accept children:
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"]
];
}Above the rule
Below the rule
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:
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."];
}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).
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."];
}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.
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"];
}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
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 hatchEverything 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.
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."];
}The dish arrived with an air of déjà vu — and for what has no typed property, a bare data-demo, written verbatim.
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.
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
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"]]];
}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.
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"];
}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:
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")
];
}Gradients via <defs> and <linearGradient> (the Rask brand mark itself is built this way); a nested
SvgTitle gives the graphic its accessible name:
namespace Rask.Site.Features;
public sealed partial class SvgGradientDemo : Component
{
protected override Component? Render() =>
RaskLogo.Size(120).GradientId("svgPageBolt");
}Clickable shapes — OnClick works on any element; the selection re-renders live over the same transport
as the rest of the page:
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);
}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:
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"]
]
];
}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:
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")
]
];
}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.
[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]:
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"], "."
]
];
}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. 漢 annotates pronunciation.
Super
Edits: added and removed.
[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]:
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."]
]
];
}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)
Unordered
Description
Div[Span["hi"]]
[h1][h1]–[h6][h6], [header][header], [footer][footer], [main][main], [section][section], [article][article], [aside][aside], [nav][nav], [address][address], [hgroup][hgroup]:
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"]
]
];
}A subtitle grouped with the heading
Headings H1–H6:
[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]:
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; }
}
}[table][table], [caption][caption], [colgroup][colgroup]/[col][col], [thead][thead]/[tbody][tbody]/[tfoot][tfoot], [tr][tr], [th][th]/[td][td]:
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"]]
]
];
}| Region | Q1 | Q2 |
|---|---|---|
| North | 10 | 12 |
| South | 8 | 15 |
| Total | 45 | |
[img][img], [picture][picture]/[source][source], [audio][audio], [video][video]/[track][track], [iframe][iframe], [embed][embed], [object][object], [canvas][canvas], [map][map]/[area][area]:
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()
]
]
];
}audio (controls)
video (poster + track)
[details][details]/[summary][summary], [dialog][dialog], [menu][menu]:
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"]]
]
]
];
}The browser shows/hides this natively; no JS needed.
menu (a semantic toolbar / command list)
[html][html], [head][head], [body][body], [title][title], [base][base], [link][link], [meta][meta], [style][style], [script][script], [noscript][noscript]:
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)"]
]
]
];
}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:
(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.