Swift Markdown Kit · DOCS

Theming & Syntax

Theming & Syntax

Colour scheme and theme

Two separate things, deliberately: colorScheme decides whether the design renders light or dark, theme decides what the design is.

var options = MarkdownRenderOptions()
options.colorScheme = .auto        // .auto follows the system; .light / .dark force it
options.theme = .github            // .system (default) or .github

Both are applied at runtime — no rebuild, no separate stylesheet.

Token overrides

A theme is a named preset plus per-token overrides, and every override takes a light and a dark value:

let theme = MarkdownTheme.github
    .textColor(light: .label, dark: .label)
    .linkColor(light: .systemIndigo, dark: .systemTeal)
    .codeBlockSurface(light: .secondarySystemBackground, dark: .tertiarySystemBackground)
    .blockquoteBorder(light: .systemGreen, dark: .systemGreen)
    .bodyLineHeight(1.55)
    .codeCornerRadius(8)
    .headingSize(level: 1, em: 1.4)
    .headingSize(level: 2, em: 1.25)

var options = MarkdownRenderOptions()
options.theme = theme

For anything the named helpers do not cover, set the underlying CSS custom property directly:

let theme = MarkdownTheme.system
    .token("--md-reasoning-color", light: .secondaryLabel, dark: .secondaryLabel)
    .token("--chat-table-border", light: "#d0d7de", dark: "#30363d")
    .metric("--md-paragraph-margin", "0.9em")
    .codeToken("--hljs-keyword", light: .systemPink, dark: .systemPink)

Three families of token: token for colours that differ between light and dark, metric for scheme-independent sizing, and codeToken for syntax highlighting.

Typography and layout

var options = MarkdownRenderOptions()
options.baseFontSize = 16
options.respectsDynamicType = true    // follow the system text size
options.minFontScale = 0.85           // clamp so accessibility sizes cannot break the layout
options.maxFontScale = 1.6
options.contentPadding = UIEdgeInsets(top: 20, left: 16, bottom: 20, right: 16)
options.bottomGap = 24                // room for a composer overlaying the bottom
options.backgroundColor = .systemBackground

For conversations there are two more, because a message bubble and the column it sits in are different widths:

var chat = MarkdownChatRenderOptions()
chat.maxMessageWidth = .points(780)             // the reading column
chat.userMessageMaxWidth = .fraction(0.82, upTo: 620)  // the user's own bubbles
chat.messageSpacing = 14

Custom inline syntax

Register inline extensions declaratively. No code crosses the bridge: each rule is a recognition pattern, and every match renders as a fully escaped .md-ext-{name} span that reports taps back to you.

var options = MarkdownRenderOptions()
options.extensions = [
    .mention(),   // @handle
    .hashtag(),   // #tag
    .ticker(),    // $AAPL — uppercase only, so "$5" is untouched
    .wikilink(),  // [[Page]]
    .prefix(name: "issue", trigger: "ISSUE-", body: .upperDigit, action: "issue"),
    .delimiter(name: "spoiler", open: "||", close: "||", action: "spoiler")
]

Two recognition modes:

  • prefix — a trigger string followed by a bounded body, matched with a character class (.word, .alnum, .letter, .upper, .upperDigit, or .chars("…")). No regex in the hot path, so no catastrophic backtracking.
  • delimiter — an opening and closing marker capturing the text between them.

Taps arrive as an action:

var options = MarkdownRenderOptions()
options.extensions = [.mention(), .wikilink()]

MarkdownRenderView(markdown: article, options: options) { event in
    guard case .action(let action) = event else { return }
    if case .extensionTapped(let name, let tapped, let value) = action {
        route(name: name, action: tapped, value: value)
    }
}

Each extension exposes its own theme tokens — --md-ext-{name}-color, -bg, -radius, -padding, -weight, -decoration — so styling goes through the same mechanism as everything else:

let theme = MarkdownTheme.system
    .token("--md-ext-mention-color", light: .systemBlue, dark: .systemTeal)
    .metric("--md-ext-mention-radius", "4px")

What cannot be themed at runtime

The baseline stylesheet — the default spacing, the code block chrome, the table rules — is compiled into the renderer. Presets and token overrides cover the visual surface; changing the baseline itself needs a rebuilt framework. If a token you need is missing, tell us and it is usually a small change.