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.