MarkdownTheme
This page covers the composables that provide and build styles. For the styleable elements and their properties, see Style properties.
A style reaches a component one of two ways: from the nearest enclosing MarkdownTheme, or from the component's own style parameter, which wins. Provide a theme once near the top of your UI and override per instance only where something has to differ.
MarkdownTheme
@Composable
fun MarkdownTheme(
style: MarkdownStyle,
content: @Composable () -> Unit,
)
Provides style as the default for everything inside content. Themes nest, and the innermost one wins:
import com.swmansion.enriched.markdown.compose.EnrichedMarkdownText
import com.swmansion.enriched.markdown.compose.MarkdownTheme
MarkdownTheme(style = appStyle) {
HomeScreen()
MarkdownTheme(style = compactStyle) {
ChatBubble(markdown = message) // uses compactStyle
}
}
style
The style every EnrichedMarkdownText inside content uses unless it passes its own. There is no default: pass MarkdownStyle.Default explicitly if you only want the library defaults.
| Type | Default |
|---|---|
| MarkdownStyle | Required |
content
The subtree the style applies to.
| Type | Default |
|---|---|
| @Composable () -> Unit | Required |
MarkdownTheme.style
object MarkdownTheme {
val style: MarkdownStyle
@Composable @ReadOnlyComposable get
}
Reads the style currently in scope. This is what EnrichedMarkdownText uses as the default for its style parameter; read it yourself when you want to derive from the ambient style rather than replace it:
import androidx.compose.ui.graphics.Color
import com.swmansion.enriched.markdown.compose.EnrichedMarkdownText
import com.swmansion.enriched.markdown.compose.MarkdownTheme
EnrichedMarkdownText(
markdown = content,
style = MarkdownTheme.style.merge { link { color = Color.Red } },
)
LocalMarkdownStyle
val LocalMarkdownStyle: ProvidableCompositionLocal<MarkdownStyle>
The staticCompositionLocalOf behind MarkdownTheme, defaulting to MarkdownStyle.Default. MarkdownTheme is a thin wrapper over providing it, so prefer the wrapper; reach for the composition local directly only when you need to provide it alongside other locals in one CompositionLocalProvider.
Because it is a static composition local, changing its value recomposes the whole subtree rather than only the readers. Provide a style that is stable across recompositions - see Building styles cheaply.
markdownStyle
fun markdownStyle(block: MarkdownStyleBuilder.() -> Unit): MarkdownStyle
Builds a style from the DSL. Not a composable, so you can - and usually should - hoist it to file scope:
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.sp
import com.swmansion.enriched.markdown.compose.markdownStyle
val AppMarkdownStyle = markdownStyle {
paragraph { fontSize = 16.sp }
link { color = Color(0xFF2563EB) }
}
Calling the same element block twice in one builder merges the two: properties set in the second call win, and the rest of the first call is kept. The result below has a 16 sp paragraph in #1A1A1A:
val Combined = markdownStyle {
paragraph { fontSize = 16.sp }
paragraph { color = Color(0xFF1A1A1A) }
}
Call it from a composable only when the values come from composition, such as MaterialTheme tokens. The block is not a composable, so read those values first and build the style inside remember, keyed on them - see Building styles cheaply. For colors from the Material scheme, rememberMaterialMarkdownStyle does this for you.
block
The DSL block. Each element function inside it (paragraph, h1, link, …) sets only the properties you assign; everything else keeps its default. See Style properties for what each element takes.
| Type | Default |
|---|---|
| MarkdownStyleBuilder.() -> Unit | Required |
materialMarkdownStyle / rememberMaterialMarkdownStyle
// com.swmansion.enriched.markdown.compose.material3
fun materialMarkdownStyle(colorScheme: ColorScheme): MarkdownStyle
@Composable
fun rememberMaterialMarkdownStyle(
colorScheme: ColorScheme = MaterialTheme.colorScheme,
): MarkdownStyle
Builds a style colored from a Material 3 ColorScheme, so Markdown matches the rest of the app. rememberMaterialMarkdownStyle reads MaterialTheme.colorScheme and rebuilds only when the scheme changes - on a light/dark switch, for example - and reuses the result otherwise.
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import com.swmansion.enriched.markdown.compose.MarkdownTheme
import com.swmansion.enriched.markdown.compose.material3.rememberMaterialMarkdownStyle
MaterialTheme(colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()) {
MarkdownTheme(style = rememberMaterialMarkdownStyle()) {
NavHost(...)
}
}
Only colors come from the scheme - sizes, fonts and spacing keep the defaults. Material 3 has no tokens for the note, tip, important and warning admonitions, so they keep GitHub's alert colors for the scheme's brightness; caution uses error. Fenced code blocks keep the default dark style in both schemes, since the code highlighting palette is made for it. Plugin styles, such as math, are not set.
To change part of the mapping, layer your overrides on top with merge, remembered on the scheme:
val colorScheme = MaterialTheme.colorScheme
val style = remember(colorScheme) {
materialMarkdownStyle(colorScheme).merge {
link { color = colorScheme.secondary }
}
}
rememberMarkdownStyle { … } is deprecated. It remembers markdownStyle { … } keyed on MaterialTheme.colorScheme, but its block is not a composable, so it cannot read MaterialTheme itself - the scheme has to be read outside it, at which point a plain remember does the same job. Use rememberMaterialMarkdownStyle or remember + markdownStyle instead.
MarkdownStyle
@Immutable
class MarkdownStyle {
fun merge(block: MarkdownStyleBuilder.() -> Unit): MarkdownStyle
fun merge(other: MarkdownStyle): MarkdownStyle
operator fun plus(other: MarkdownStyle): MarkdownStyle
companion object {
val Default: MarkdownStyle
}
}
An immutable, layered style. It holds a stack of override layers rather than a finished stylesheet, and the layers are flattened onto the library defaults only when a component resolves them.
MarkdownStyle.Default
The empty style - no layers, so every element keeps its library default. It is what LocalMarkdownStyle starts out as, so an EnrichedMarkdownText with no theme at all renders with the library defaults - which are a fixed light palette (see Dark mode).
Dark mode
The library defaults are a fixed light palette: dark gray text, a blue link, light table and blockquote backgrounds. They do not follow the system theme, so with no style of your own the text is hard to read on a dark background. Supply the colors for the current theme yourself - from the Material color scheme through rememberMaterialMarkdownStyle, or as two hoisted variants picked by isSystemInDarkTheme():
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import com.swmansion.enriched.markdown.compose.MarkdownTheme
import com.swmansion.enriched.markdown.compose.markdownStyle
val LightMarkdownStyle = markdownStyle {
paragraph { color = Color(0xFF1A1A1A) }
}
val DarkMarkdownStyle = markdownStyle {
paragraph { color = Color(0xFFE0E0E0) }
link { color = Color(0xFF60A5FA) }
code {
color = Color(0xFFF9A8D4)
backgroundColor = Color(0xFF3B1D2A)
borderColor = Color(0xFF5B2A3E)
}
table {
color = Color(0xFFE0E0E0)
headerBackgroundColor = Color(0xFF1F2937)
headerTextColor = Color(0xFFF9FAFB)
rowEvenBackgroundColor = Color(0xFF111827)
rowOddBackgroundColor = Color(0xFF1F2937)
borderColor = Color(0xFF374151)
}
}
@Composable
fun ThemedMarkdown(content: @Composable () -> Unit) {
MarkdownTheme(
style = if (isSystemInDarkTheme()) DarkMarkdownStyle else LightMarkdownStyle,
content = content,
)
}
Every element with a color of its own - inline code, blockquotes, tables, list markers, task lists - needs a dark value too. Code blocks are the exception: their default is already a dark block, and so is the palette of the optional code highlighting. See Style properties for each element's color properties.
MarkdownStyle.merge
fun merge(block: MarkdownStyleBuilder.() -> Unit): MarkdownStyle
fun merge(other: MarkdownStyle): MarkdownStyle
operator fun plus(other: MarkdownStyle): MarkdownStyle
Returns a new style with block added as a layer on top. It does not rebuild the style or replace what came before. Later layers win per property, and properties no layer sets keep their defaults. That makes merge the natural way to express variants:
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.sp
import com.swmansion.enriched.markdown.compose.markdownStyle
val Base = markdownStyle {
paragraph { fontSize = 16.sp }
link { textDecoration = TextDecoration.None }
}
val Light = Base.merge { paragraph { color = Color(0xFF1A1A1A) } }
val Dark = Base.merge { paragraph { color = Color(0xFFE0E0E0) } }
Light and Dark both keep the base font size and links without an underline (links are underlined by default).
To layer one whole style onto another, pass it to merge or use +. The right-hand style's layers go on top, and anything it leaves unset keeps the left-hand style's value:
val BrandColors = markdownStyle { link { color = Color(0xFF7C3AED) } }
val Branded = Base + BrandColors // same as Base.merge(BrandColors)
Two styles are equal when their layers are equal, so a hoisted style stays equal across recompositions and does not retrigger work downstream.
How styles resolve
A MarkdownStyle is not a set of final pixel values. Resolving one means folding its layers onto StyleConfig.default(context) while converting Compose types to what the underlying view wants: sp and Dp become pixels at the current density, Color becomes a packed color int, and a FontFamily is resolved to a typeface.
That depends on the device configuration, so EnrichedMarkdownText re-resolves whenever the style, density, font resolver, or configuration changes, and it does the work off the main thread. The first frame paints with the library defaults and the resolved style lands immediately after.
Building styles cheaply
Two consequences worth designing around:
- Hoist your styles. Building a style inside a composable body allocates a new
MarkdownStyleon every recomposition. Equal layers still compare equal, so nothing re-resolves - but it is free to avoid, andLocalMarkdownStylebeing a static composition local makes an unstable theme value expensive. - Remember styles whose values come from composition. Read the values in the composable, then build the style in
remember, keyed on them, so it is rebuilt only when they change. For the Material color scheme,rememberMaterialMarkdownStyledoes this for you.
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.sp
import com.swmansion.enriched.markdown.compose.MarkdownTheme
import com.swmansion.enriched.markdown.compose.markdownStyle
// Hoisted: allocated once for the whole process.
val AppMarkdownStyle = markdownStyle {
paragraph { fontSize = 16.sp }
link { color = Color(0xFF2563EB) }
}
@Composable
fun Root() {
// Read from composition, so it has to be remembered rather than hoisted.
val colorScheme = MaterialTheme.colorScheme
val themed = remember(colorScheme) {
AppMarkdownStyle.merge { paragraph { color = colorScheme.onSurface } }
}
MarkdownTheme(style = themed) { Content() }
}
See also
- Style properties - the elements and properties you can set.
EnrichedMarkdownText- thestyleparameter.- Custom fonts - how
FontFamilyvalues are resolved.