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"
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 Head 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-ui-muted")["(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: {DateTime.Today: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.
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($"{Tw.Blockquote} text-base")["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")[
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; }
}
}
Tables:
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"]]]
]
];
}
| # | 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, …) have SelfClosing => true 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() =>
Div
.Id("card-1")
.Class($"{Tw.Card} ring-violet-500")
.Style("padding: 0.6rem 0.8rem;")["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).
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."];
}
Aria, Role, TabIndex — Aria 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.
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"];
}
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 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(new Dictionary<string, string?> { ["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[
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"]]];
}
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.
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"];
}
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:
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:
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;
}
}
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"]
]
];
}
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.
[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 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"], "."
]
];
}
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. 漢 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-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."]
]
];
}
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-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"]
]
];
}
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.)
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; }
}
}
[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($"{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"]]
]
];
}
| 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")[
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)
]
]
]
];
}
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(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"]]
]
]
];
}
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("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)"]
]
]
];
}
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:
(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.