Skip to content

Markdown

The Compose UI components SDK can render message text as markdown, so a message written with **bold** or a > quote displays formatted instead of showing its syntax.

Markdown is off by default. Opt in by replacing the message text formatter on ChatTheme:

val config = ChatUiConfig()

ChatTheme(
    config = config,
    messageTextFormatter = MessageTextFormatter.markdownFormatter(
        autoTranslationEnabled = config.translation.enabled,
    ),
) {
    // Your UI content
}

ChatTheme's default is untouched, so nothing renders differently until you pass this.

Pass autoTranslationEnabled the same value the theme's config.translation.enabled holds, as the default formatter does. Hardcoding it leaves the formatter resolving translations, or failing to, while the rest of the UI does the opposite.

The View-based SDK has its own markdown support through a separate module. See Formatting for that one.

What is rendered

Construct Result
*italic*, **bold**, ***both*** Emphasis
~~strikethrough~~ Struck through
`code` Monospace with a subtle background
Fenced and indented code blocks Monospace block, no syntax highlighting
# Heading through ###### Heading Six levels, mapped to the type scale
- item, 1. item, and nesting Marker per item, indented per level
> quote, including nesting Indented with a rail drawn beside it
[text](url), [text][ref], and bare URLs Tappable links
--- A thematic break
Two trailing spaces, a trailing \, or <br/> A hard line break

Four constructs are not drawn, because none of them can be expressed in a styled string:

  • An image renders as its alt text, which is the specification's own fallback for an image that cannot be shown.
  • A table keeps its source text.
  • A task list has no checkboxes. Its items render as an ordinary bulleted list with the marker left as text, so - [x] done reads as • [x] done.
  • HTML is not interpreted. A block keeps its source and an inline tag is shown as written, so <b>bold</b> reads as typed. The one exception is <br/>, which breaks the line as the table above says.

Anything the renderer does not recognise falls back to the text as typed, so no content is dropped.

Line breaks

A single line break renders as a line break. The specification collapses it to a space and asks for two trailing spaces to force one, which nobody can type on a phone keyboard, and following it would reflow every multi-line message that reads correctly as plain text today. The View-based SDK deviates the same way.

Styling

Markdown is styled from the typography and colors you pass to markdownFormatter, which default to the SDK's own. It does not read them from ChatTheme, because the formatter is built before the theme provides anything. So if you customise either on ChatTheme, pass the same values through:

val config = ChatUiConfig()
val colors = StreamDesign.Colors.default().copy(textPrimary = Color.Black)

ChatTheme(
    config = config,
    colors = colors,
    messageTextFormatter = MessageTextFormatter.markdownFormatter(
        autoTranslationEnabled = config.translation.enabled,
        colors = colors,
    ),
) {
    // Your UI content
}

Headings use four sizes for six levels, so two pairs share one. The third and fourth levels render identically, and the fifth and sixth differ only in colour.

Markdown renders first, then mentions, links and emails are highlighted over the result. So a mention still highlights in a message that also contains markdown, and a URL inside a code span is deliberately left alone rather than linkified.

Markdown inside a mention token is a different matter: writing @**Name** renders the name bold, but the composer drops the mention, because it looks for the @Name token in the text you are sending and @**Name** no longer contains it.

Only http, https, mailto and tel destinations become links. Message text comes from other people and a tapped link is handed to the system, so a link reading as ordinary prose must not be able to open a javascript: target or deep link into your app.

Where markdown is not rendered

Only the message list renders markdown. Quoted replies, channel list previews, thread and search previews, and the composer's edit indicator continue to show the markdown source, since they do not go through messageTextFormatter.

Combining with other formatters

markdownFormatter replaces the default formatter and cannot be combined through MessageTextFormatter.composite. Rendering changes the text's length, so the character offsets a composed formatter styles by no longer line up with Message.text.

If you need markdown plus your own styling, wrap the formatter and post-process the AnnotatedString it returns.