Markdown
Renders markdown as themed HTML, as a document or as an inline label, with an optional source view.
Live demo
Markdown renders GitHub-Flavored Markdown into HTML that themes entirely from the token register: headings ride the type scale, rules and quotes ride the border and surface ramps, and fenced code borrows the same --code-* family the Code component owns, so a fence inside a document and an <xtyle-code> beside it agree in any theme. GFM is on — tables, task lists, strikethrough, and autolinks all render.
inline switches to a label render: emphasis, code, links and strikethrough, but no blocks and no paragraph wrapper, so it drops into a tab title or a chip and inherits its type — a generated label that opens with # stays text instead of erupting a heading into a tab strip. editable adds a source view the reader can switch to, emitting input as it is typed. It ships no sanitizer, by design. Raw HTML in the source is escaped to text rather than rendered, and link and image URLs are written from a scheme allowlist, so everything that reaches the DOM is markup the renderer generated itself from a closed token set — the arbitrary-HTML problem never arises rather than being solved. allowHtml lifts the escaping for a source the app controls, and it is not the hole it sounds like: the body reaches the DOM through the component's fragment, so the format declared in component-host.json still has the last word and refuses <script>, event handlers, and elements outside xtyle's vocabulary no matter what the renderer emits. What survives is standard HTML and xtyle's own components, so a document can carry an <xtyle-badge> or an <xtyle-alert> inline. URL schemes are separate and app-wide rather than per-element: allowUriSchemes() from @xtyle/core/elements widens the renderer and the fragment format together, which is the only way to widen either — and xtyle adds no scheme on an app's behalf, because a hole nobody asked for is the worst kind.
When to use
How this component composes with the rest of the set.
Props
6 props, straight from the manifest.
| Prop | Type | Default | Bindings | Description |
|---|---|---|---|---|
Appearance
States
editing
The source view is showing: the body is swapped for the textarea and the toggle reads as pressed.
inert-link
A link whose URL the renderer refused — a javascript: href, say. It keeps its text but loses its href, and reads dimmed and unclickable rather than lying about being a link.
Anatomy
The named parts that make up the component, with their selectors.
body
Where the rendered markdown lands. Every rule is scoped inside it and styles element types rather than named parts, since the content's structure is the author's, not ours — so nothing here reaches a consumer's own headings. Carries data-allow-html while allowHtml is set, so a body holding the author's own markup is recognizable in a DevTools inspection or a grep of the rendered page.
heading
Document headings, on the type scale; h1 and h2 carry a rule beneath them.
inline-html
The HTML elements allowHtml makes reachable — mark, kbd, abbr, sub/sup, details/summary. Themed from the register rather than left to the browser, which would otherwise paint a mark in fixed yellow with no idea what theme it is in, and style a kbd unlike the Kbd component sitting next to it.
code
Inline code and fenced blocks, borrowing the Code component's --code-fg / --code-bg rather than deriving a second opinion about what code looks like.
table
A GFM table: striped rows, a filled header, and its own horizontal scroll so a wide table never pushes the document sideways.
quote
A blockquote, marked by a leading edge and dimmed a step off the body text.
editor
The source textarea shown while editing, in the mono face on the field surface.
toggle
The edit/view switch. Chrome the component invents, so it is a real node in the fragment fill: a mod can reword it, make it an icon, or move it, and the element keeps working.
Tokens & coverage
What the component consumes, checked live against what the algorithm produces.
Live coverage check against the xtyle-default register
(derive(xtyleDefault, { anchors }) →
coverComponent(manifest, register)). Every token this component
consumes must be a key the algorithm produces.
--accent
--accent-text
--bg-1
--bg-2
--border-thick
--border-thin
--code-bg
--code-fg
--fg-0
--fg-1
--fg-2
--field-border
--font-mono
--font-sans
--leading-normal
--leading-tight
--line-2
--radius-md
--radius-sm
--space-1
--space-2
--space-3
--space-4
--space-5
--space-6
--text-2xl
--text-body
--text-lg
--text-sm
--text-xl
--text-xs
--warn-bg
--warn-text
--weight-bold
--weight-semibold
Slots
The markdown as text content (html/svelte). For Astro, pass it via the source prop instead.
Accessibility
Code
A document
The block render: headings, emphasis, links and lists, themed from the register.
<xtyle-markdown source="## Release notes
The **tooltip** now tracks its trigger. See [the changelog](/changelog).
- fixed placement under scroll
- `Escape` dismisses a hover-raised hint"></xtyle-markdown>
<script lang="ts">
import { Markdown } from "@xtyle/svelte";
const notes = `## Release notes
The **tooltip** now tracks its trigger. See [the changelog](/changelog).`;
</script>
<Markdown source={notes} />
---
import Markdown from "@xtyle/astro/Markdown.astro";
const notes = `## Release notes
The **tooltip** now tracks its trigger. See [the changelog](/changelog).`;
---
<Markdown source={notes} />
An inline label
inline renders emphasis without blocks and without a paragraph wrapper, so a generated tab title or chip inherits its surroundings. Block syntax stays literal — a leading # is text, not a heading.
<!-- a label, not a document: emphasis renders, blocks stay literal -->
<xtyle-markdown inline source="Fix **tooltip** in `AnchorTracker`"></xtyle-markdown>
<script lang="ts">
import { Markdown } from "@xtyle/svelte";
// a tab title an LLM formatted, dropped straight into a tab strip
export let title = "Fix **tooltip** in `AnchorTracker`";
</script>
<Markdown inline source={title} />
---
import Markdown from "@xtyle/astro/Markdown.astro";
---
<Markdown inline source="Fix **tooltip** in `AnchorTracker`" />
Tables and task lists
GFM is on: tables, task lists, strikethrough and autolinks all render. A wide table scrolls inside its own box.
<xtyle-markdown source="| component | state |
| --- | --- |
| tooltip | fixed |
| swatch | fixed |
- [x] track the anchor
- [ ] name the version"></xtyle-markdown>
<script lang="ts">
import { Markdown } from "@xtyle/svelte";
</script>
<Markdown source={`| component | state |
| --- | --- |
| tooltip | fixed |
- [x] track the anchor
- [ ] name the version`} />
---
import Markdown from "@xtyle/astro/Markdown.astro";
---
<Markdown source={`| component | state |
| --- | --- |
| tooltip | fixed |
- [x] track the anchor`} />
Markdown you wrote yourself
allow-html renders the source's markup, so a document can carry an <xtyle-badge> and have it theme like any other. The fragment format still refuses scripts, handlers, and undeclared elements. A host protocol like asset: is a separate, app-wide decision: allowUriSchemes at startup, which widens the renderer and the fragment format together.
<!-- markdown the app wrote, so its markup renders — components included -->
<xtyle-markdown allow-html source="## Shipped
<kbd>Ctrl</kbd>+<kbd>K</kbd> opens the palette. <xtyle-badge tone="success">new</xtyle-badge>"></xtyle-markdown>
<!-- a host protocol is app-wide, declared once at startup rather than per element -->
<script type="module">
import { allowUriSchemes } from "@xtyle/core/elements";
allowUriSchemes("asset", "tauri");
</script>
<script lang="ts">
import { Markdown } from "@xtyle/svelte";
// bundled with the app, so the markup in it is the app's own
import notes from "./release-notes.md?raw";
</script>
<Markdown allowHtml source={notes} />
---
import Markdown from "@xtyle/astro/Markdown.astro";
const notes = await Astro.glob("../content/release-notes.md");
---
<!-- build-time content from the repo: the HTML in it is ours -->
<Markdown allowHtml source={notes[0].rawContent()} />
An editable source view
editable adds a toggle between the render and its markdown source, emitting input with the source as it's typed.
<xtyle-markdown editable source="# Draft
Switch to the source and edit it."></xtyle-markdown>
<script lang="ts">
import { Markdown } from "@xtyle/svelte";
let draft = "# Draft\n\nSwitch to the source and edit it.";
</script>
<Markdown editable source={draft} on:input={(e) => (draft = e.detail.source)} />
---
import Markdown from "@xtyle/astro/Markdown.astro";
---
<Markdown editable source={"# Draft\n\nSwitch to the source and edit it."} />