Summary#
Markdown is how people write text that has shape — a heading, a list, a link, a table. Markdown(text) takes such a
string and renders it:
Markdown(article.Body)It is a renderer, not an editor. It has no toolbar, no cursor and no storage: you give it a string and it draws the document that string describes. That makes it the right thing for the many places that want formatted text and nothing else — a product description, a help panel, a policy page, release notes, an agent's reply.
The rendered document inherits your app's theme tokens tokens, so its headings use your heading face, its links take your primary colour, and it follows your app through light and dark without being told to.
Signature#
Markdown(<string expression>) // render a settled document
Markdown(text, streaming: <bool>) // the document is still arriving
Markdown(text, copyIcon: <icon>, copyTooltip: "…") // give every code block a copy buttonThe first argument is an ordinary expression — a property, a member, a literal, the result of a call. There is nothing to declare and nothing to register. The two options are described below and both default to off.
Description#
It renders whatever the string says#
Everything below is understood. Anything else is shown as the text the author typed.
| Headings | # One … ###### Six, and the underlined form. A trailing {#custom-id} becomes the heading's id, so a #link can land on it |
| Text | *emphasis*, **strong**, ~~struck~~, `code`, and a line ending in two spaces for a hard break |
| Lists | bullets and numbers, nested, tight or spaced; - [ ] / - [x] render a checkbox |
| Blocks | fenced code with its language, indented code, block quotes, horizontal rules |
| Tables | the pipe form, including per-column alignment |
| Links | [text](url), bare <https://…>, <name@example.com>, and images  |
| Alerts | > [!NOTE] and TIP, IMPORTANT, WARNING, CAUTION — the GitHub callout boxes |
Alerts — a callout box
A block quote whose first line is a marker on its own becomes a coloured callout:
> [!WARNING]
> This deletes the record and cannot be undone.The marker is case-insensitive, and the title is supplied for you — the five kinds are the whole vocabulary, and
NOTE, TIP, IMPORTANT, WARNING and CAUTION are exactly the ones GitHub renders, so a document written for
one reads the same in the other.
A marker that is not one of the five is not an alert. > [!WARNIGN] stays an ordinary quote with its marker
visible, which is what every reader that has never heard of alerts already does. That is deliberate: a typo turning
into a confident box titled "Warnign" would look intentional, and a wrong-looking callout is worse than a plain
quote.
It renders as a box with its own role rather than a tinted quote, because a quote means someone else said this and
an alert means the author is raising their voice — and a screen reader announces the two differently. Each kind
takes its colour from one token (Colors { Markdown { AlertWarning } }), which sets the bar, the title and the
tint together.
⚠ The markdown editor does not yet style these — it round-trips them safely and shows the marker as text, so a document is never damaged by being edited there; it just does not draw the box.
Three things are deliberately NOT rendered. Raw HTML is shown as text — see below, it is the security rule.
Footnotes ([^1]) are not yet formatted and appear as written; the markdown editor does handle them, so a
document may contain one. Math ($x^2$, $$…$$) is the same: the editor renders formulae, this renderer shows
their source. That difference is deliberate rather than pending — drawing maths needs a typesetting library, and this
renderer is part of the platform's client, which carries no third-party code at all. An editor is an app's own
component and can choose to ship one.
A rule under the title
Long documents read better when the title area closes off. Set one token and the document's opening heading gets a hairline underneath:
theme Default {
Colors { Markdown { HeadingRule = "#E5E3DC"; } }
Space { Markdown { HeadingRuleGap = "0.3em"; } } // optional breathing room; omit for a flush line
}The LEADING h1 only — the document's title. A second h1 further down takes no line, and neither does an
h2: this closes the title area, it is not heading decoration. A document that does not open with an h1 gets no
rule at all, which is correct — it has no title area. Use HeadingRuleGap to make the line close the title area
rather than underline the words.
It is off unless you ask for it, and off costs nothing. With no HeadingRule declared the headings are exactly
what they were: no line, and — the part that matters — no space reserved for one. A document you are already
rendering does not move by a pixel because this feature exists.
⚠ Do not confuse this with a --- in the text. That is a thematic break, part of the document, and it renders
as a full-width rule wherever the author put one (Colors { Markdown { Rule } }). The heading rule is your theme's
opinion about headings; the thematic break is the author's content. A tool like Craft draws both, which is why they
can look like the same feature.
A document is content, never markup#
This is the rule worth knowing, because it is what makes the atom safe to point at data:
Anything in the document that looks like HTML reaches the page as text.
<b>renders as the four characters<b>, not as bold. A<script>is four-and-a-bit characters of visible text, not a script.
Text in a database is written by people, and on this platform sometimes by agents. If a stored description could
smuggle a <script> into the page that shows it, then every page that shows a description would be a hazard. So it
cannot: there is no setting that turns raw HTML on.
It also happens to be what authors mean. A sentence containing List<string> renders as a sentence containing
List<string>, rather than losing the word to a tag nobody wrote.
Links are held to the same rule: a URL that is not an ordinary web link is rendered inert rather than followed, so
[click](javascript:…) shows the word "click" and goes nowhere.
Styling it#
Placing it. The atom takes the same style props props as anything else — most often a reading measure:
Markdown(article.Body, maxW: 672, mx: "auto")Its look, in three tiers. You will usually only need the first.
- Do nothing. The document reads in your app's voice already: it takes
Fonts { Heading }for its headings,Colors { Primary }for its links,Colors { Muted }behind code,Colors { Border }for rules and table borders, and your radii. - Move a semantic token and the document moves with everything else — change
Colors { Primary }and its links follow, because that is where they came from. - Give the document its own value by nesting a
Markdowngroup inside the token's category. A nested group namespaces within that category, so it never disturbs the token it overrides for everyone else:
theme Docs {
Colors {
Primary = "#0F766E"; // the app's links, buttons, focus rings
Markdown { Link = "#B45309"; } // …but a DOCUMENT's links, only
}
FontSize { Markdown { H1 = "2.4rem"; H2 = "1.7rem"; H3 = "1.3rem"; } }
FontWeight { Markdown { HeadingWeight = 700; } }
Space { Markdown { Block = "1.25em"; HeadingTop = "2em"; ListIndent = "1.8em"; } }
Length { Markdown { LineHeight = "1.75"; } }
}The full set, each falling back to the app token in brackets and then to a built-in default:
| Category | Keys |
|---|---|
Colors { Markdown { … } } | Link (Primary) · CodeBg (Muted) · TableHeadBg (Muted) · TableBorder (Border) · Rule (Border) · QuoteBar (Border) · QuoteText (TextMuted) · AlertNote · AlertTip · AlertImportant · AlertWarning · AlertCaution · HeadingRule |
FontSize { Markdown { … } } | H1 · H2 · H3 · H4 · CodeSize |
FontWeight { Markdown { … } } | HeadingWeight · TableHeadWeight · AlertTitle |
Fonts { Markdown { … } } | HeadingFace (Fonts.Heading) · CodeFace (Fonts.Mono) |
Space { Markdown { … } } | Block · HeadingTop · HeadingBottom · ListIndent · ItemGap · CodePad · CellPad · QuotePad · RuleGap · AlertPad · AlertTitleGap · HeadingRuleGap |
Length { Markdown { … } } | LineHeight · HeadingLineHeight · QuoteBarW · AlertBarW · HeadingRuleW |
Radius { Markdown { … } } | CodeRadius (Radius.Sm) · BlockRadius (Radius.Md) |
Token names are global. A theme's leaf names must each denote one value — a bare
Headingin a variant has to mean something definite — so a nested group namespaces the variable, not the name. That is why these readHeadingFaceandHeadingWeightrather thanHeadingtwice: an app almost certainly already hasFonts { Heading }, and a collision is a compile error naming both groups.
Rendering a reply while it is still streaming in#
An agent's reply arrives a few characters at a time, and a document being typed is briefly not valid markdown: the
** of a bold run has no closing pair yet, a | is not yet a table. Rendering that literally shows the reader the
raw syntax for a frame or two, which looks like a glitch.
Say the text is still arriving and the renderer holds the unfinished tail — an in-progress construct is drawn as though it were already closed, and a caret marks the end:
component Reply(string Body, bool Done) {
render {
Markdown(Body, streaming: !Done);
}
}Two things are worth knowing:
streaming: falseis the half that matters. The renderer can tell that text was appended, so it can stabilise a half-typed construct by itself. What it cannot tell is that the stream has ended — so turning the flag off is what settles the document, re-rendering the tail as ordinary markdown. A document left permanentlystreaming: truekeeps its caret forever.- It does not make rendering faster. Rendering is already incremental: editing one paragraph of a long document repaints that paragraph, whether or not anything is streaming.
A copy button on every code block#
A document full of commands or snippets wants a copy button on each one. Your app cannot reach inside the rendered document to add one, so ask for it here — and supply the icon and the words yourself, because they belong to your design and your language:
component Guide(string Body) {
render {
Markdown(Body, copyIcon: copy, copyTooltip: "Copy code");
}
}copyIcon is one of your app's own icons — a bare name, checked at compile time like any other icon, so a glyph you have not declared is a compile error rather than an empty square on the page. The
tooltip is also the button's accessible name, which is why the two are required together: an icon with no
tooltip is a button nobody using a screen reader can identify, and a tooltip with no icon describes a control that
never appears.
It is opt-in on purpose. A markdown atom is otherwise inert output — no buttons, no state — and that is a large part of why it is safe to point at text an agent or a customer wrote. A help panel should not sprout controls because it happened to contain a fenced block.
The button appears on hover, and on keyboard focus so it is reachable without a mouse. On touch, where there is no hover, it is always visible. To copy text that is not inside a rendered document, use Clipboard.
Where the text comes from#
Anywhere. A Markdown property on an entity is the common case — a document-backed member whose text is stored as
sections — but a plain string works exactly as well, and so does a value your code just computed.
Examples#
A product description rendered under its name:
entity Article {
[Required] [MaxLength(200)] string Title;
string Body;
security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}
[Page("/article/{id}")]
[Render(SSR)]
component ArticlePage(Guid id) {
var article = Article.Where(a => a.Id == id).FirstOrDefault();
render {
Stack(gap: 3, maxW: 720, mx: "auto") {
Text(article.Title, fontSize: 32);
Markdown(article.Body);
}
}
}Text the page itself holds — a help panel that opens and closes, with no storage behind it at all:
[Page("/help")]
[Render(CSR)]
component HelpPage() {
bool open = false;
string help = "## Getting started\n\n1. Create a project\n2. Add a page\n3. Compile\n\nSee the **guide** for more.";
action Toggle() { open = !open; }
render {
Stack(gap: 2) {
Button(open ? "Hide help" : "Show help", onPress: Toggle);
if (open) { Markdown(help, maxW: 640); }
}
}
}See also#
- Clipboard —
Clipboard.Copy(text), for copying anything outside a rendered document - theme tokens — the tokens a rendered document reads for its typefaces and colours.
- style props — the style props the atom takes, like any other element.
- entity members — declaring the property the text lives in.
- component — the component the atom is written inside.