
Syntax highlighting for Compose UIs using tree-sitter with an incremental engine for editor-grade performance; material bindings, editable highlighted text field, selectable viewer, and language modules.
Compose Multiplatform syntax highlighter for Android (and any JVM-based Compose target). Built on tree-sitter for accurate, fast highlighting — with an incremental engine for editor scenarios.
This is still an experimental project. Android is supported via the published AAR; the JVM artifact ships for Compose Desktop interop; iOS is in preview (iosArm64 klibs only — Apple Silicon devices; simulator publishing waits on the next ktreesitter release). For browser apps prefer a JS-side highlighter (e.g. highlight.js, Shiki) and keep this library on JVM-based targets.
API documentation: https://mataku.github.io/ComposeSyntaxHighlighter/ (latest release)
Read-only viewer (:material3) |
Text field (:material3-text-field) |
|---|---|
![]() |
| Platform | Status | Notes |
|---|---|---|
| Android | available | minSdk 26 |
| Desktop JVM | available | KTreeSitter native lib must be on java.library.path
|
| iOS | preview | iosArm64 klibs; device only (simulator blocked on ktreesitter) |
| Web (wasmJs) | not in scope | use highlight.js / Shiki on the JS side |
| Module | Artifact | Latest |
|---|---|---|
:core |
compose-syntax-highlight-core |
|
:material3 |
compose-syntax-highlight-material3 |
|
:material3-text-field |
compose-syntax-highlight-material3-text-field |
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}Add a Material binding plus whichever language modules you need:
// build.gradle.kts (commonMain)
// Material3 binding — ships SyntaxHighlightedText. Pulls in :core transitively.
implementation("io.github.mataku:compose-syntax-highlight-material3:$latestVersion")
// Language artifacts — one per language you want to highlight.
implementation("io.github.mataku:compose-syntax-highlight-kotlin:$latestHighlightKotlinVersion")
implementation("io.github.mataku:compose-syntax-highlight-swift:$latestHighlightSwiftVersion")For an editable code surface, depend on :material3-text-field instead of (or alongside) :material3 — it ships SyntaxHighlightedTextField:
implementation("io.github.mataku:compose-syntax-highlight-material3-text-field:$latestVersion")Each language module is published with its own version, independent from the
core stack. When the versions on your classpath are incompatible — for example,
upgrading the core stack across a major bump without also upgrading language
modules — Gradle reports a strictly conflict on
compose-syntax-highlight-api. See
docs/compatibility.md for the matrix of compatible
versions per artefact.
If you build your own Text on top of the produced AnnotatedString, or you only need highlight() to feed an existing UI, depend on :core directly:
implementation("io.github.mataku:compose-syntax-highlight-core:$latestVersion")
implementation("io.github.mataku:compose-syntax-highlight-kotlin:$latestHighlightKotlinVersion"):core ships highlight(), rememberHighlightedString(), the SyntaxTheme data class, and LocalSyntaxTheme, with only compose.runtime and compose.ui as dependencies.
import io.github.mataku.compose.highlight.api.Languages
import io.github.mataku.compose.highlight.kotlin.kotlin
import io.github.mataku.compose.highlight.material3.SyntaxHighlightedText
@Composable
fun MyScreen() {
SyntaxHighlightedText(
code = """
fun main() {
println("hello")
}
""".trimIndent(),
language = Languages.Kotlin,
)
}The default theme is SyntaxTheme.DarkDefault. To override it for a single call, pass theme directly:
SyntaxHighlightedText(
code = code,
language = Languages.Kotlin,
theme = SyntaxTheme.LightDefault
)Or override for an entire subtree with CompositionLocalProvider:
import io.github.mataku.compose.highlight.core.LocalSyntaxTheme
import io.github.mataku.compose.highlight.core.SyntaxTheme
CompositionLocalProvider(LocalSyntaxTheme provides SyntaxTheme.LightDefault) {
SyntaxHighlightedText(code = code, language = Languages.Kotlin)
}SyntaxHighlightedText paints theme.background behind the text when it is set (every built-in theme defines one). Layering, from outside in, is modifier → background → contentPadding → text, so use modifier for outer margin and contentPadding for the inset between the background edge and the text:
SyntaxHighlightedText(
code = code,
language = Languages.Kotlin,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp), // outside the background
contentPadding = PaddingValues(16.dp), // inside the background
)To suppress the background and paint your own container, override the theme:
SyntaxHighlightedText(
code = code,
language = Languages.Kotlin,
theme = SyntaxTheme.DarkDefault.copy(background = null),
)SyntaxHighlightedText wraps its output in a SelectionContainer by default, so users can highlight the rendered code with the platform's native text-selection UI (which also surfaces a Copy action on Android and Desktop). Pass selectable = false to render without a selection scope:
SyntaxHighlightedText(
code = code,
language = Languages.Kotlin,
selectable = false,
)For an editable syntax-highlighted text surface, use SyntaxHighlightedTextField from :material3-text-field:
val state = remember { TextFieldState(initialText = "val greeting = \"Hello\"") }
SyntaxHighlightedTextField(
state = state,
language = Languages.Kotlin,
theme = SyntaxTheme.DarkDefault,
modifier = Modifier.fillMaxSize(),
)The Composable is backed by IncrementalHighlighter from :core: typing
recomputes only the edited byte range, so highlighting stays interactive on
multi-thousand-line files. For non-Material3 chrome or custom layouts, use
rememberSyntaxHighlightedString directly:
val highlighted = rememberSyntaxHighlightedString(state, Languages.Kotlin)
// drop `highlighted.value` into your own overlay / Text composition.All themes are static SyntaxTheme values on SyntaxTheme.Companion, shipped in compose-syntax-highlight-core. Attributions for the third-party themes are bundled in the artifact's META-INF/NOTICE.
| Theme | Variant | Inspired by / source |
|---|---|---|
SyntaxTheme.DarkDefault |
dark | VSCode-inspired neutral palette (default for LocalSyntaxTheme) |
SyntaxTheme.LightDefault |
light | VSCode-inspired neutral palette |
SyntaxTheme.SolarizedDark |
dark | Solarized by Ethan Schoonover |
SyntaxTheme.SolarizedLight |
light | Solarized by Ethan Schoonover |
SyntaxTheme.GitHubDark |
dark | GitHub Primer syntax tokens |
SyntaxTheme.GitHubLight |
light | GitHub Primer syntax tokens |
SyntaxTheme.OneDark |
dark | Atom One Dark (one-dark-syntax) |
SyntaxTheme.OneLight |
light | Atom One Light (one-light-syntax) |
SyntaxTheme.Dracula |
dark only | Dracula (no canonical light variant) |
Each capture has its own named field, so the IDE autocompletes them — no string keys to remember:
val myTheme = SyntaxTheme(
baseStyle = SpanStyle(color = Color.White),
keyword = SpanStyle(color = Color.Magenta, fontWeight = FontWeight.Bold),
string = SpanStyle(color = Color.Yellow),
// ...
)Unset fields fall back to a parent prefix — setting keyword covers keyword.return, keyword.function, etc., and string.escape falls back to string when stringEscape is unset. For grammar-specific captures not covered by a field (e.g. keyword.return, variable.member), use extras:
val myTheme = SyntaxTheme(
keyword = SpanStyle(color = Color.Magenta),
extras = mapOf("keyword.return" to SpanStyle(color = Color.Red)),
)extras wins over typed fields for the same name.
SyntaxHighlightedText is synchronous by default — fine up to a few
hundred lines. For larger inputs, opt into the async path or
IncrementalHighlighter; see docs/performance.md
for behaviour, code examples, and benchmark methodology.
full highlight medians (warm), Kotlin:
| Runtime | 100 lines | 1k lines | 5k lines |
|---|---|---|---|
| Host JVM (heap 2g) | 5.0 ms | 48.8 ms | 239.8 ms |
| Android (BenchmarkRule, FTL) | 3.1 ms | 30.3 ms | 143.1 ms¹ |
Environment:
¹ Flagship-class only — see docs/performance.md.
Realistic code samples (~75–150 lines, single machine). First use includes one-time tree-sitter query compilation; subsequent use reflects normal app cost.
| Language | Lines | Min (ms) | Median (ms) | Max (ms) |
|---|---|---|---|---|
| Kotlin | 100 | 1083.737 | 1150.319 | 1176.068 |
| Swift | 100 | 728.042 | 751.514 | 760.699 |
| Ruby | 100 | 397.201 | 412.628 | 440.905 |
| Rust | 100 | 391.256 | 411.147 | 412.385 |
| Python | 100 | 370.287 | 372.377 | 467.543 |
| Go | 100 | 320.408 | 325.528 | 355.918 |
| Java | 100 | 341.338 | 362.281 | 408.819 |
| Language | Lines | Min (ms) | Median (ms) | Max (ms) |
|---|---|---|---|---|
| Kotlin | 100 | 4.707 | 4.959 | 5.053 |
| Swift | 100 | 3.539 | 3.716 | 3.775 |
| Ruby | 100 | 3.424 | 3.505 | 3.511 |
| Rust | 100 | 3.356 | 3.437 | 3.452 |
| Python | 100 | 3.390 | 3.471 | 3.474 |
| Go | 100 | 2.341 | 2.346 | 2.353 |
| Java | 100 | 3.040 | 3.153 | 3.205 |
Environment: Apple M3 Pro, OpenJDK 21, heap 2g, warmup 100 / measure 50.
Full config and methodology: docs/performance.md.
Run ./gradlew :benchmarks:jvm:jvmTest to reproduce on your own machine.
MIT. Bundled grammars are all MIT-licensed:
The built-in SyntaxTheme palettes shipped in compose-syntax-highlight-core derive their colors from the following third-party themes (all MIT-licensed):
Full attributions are in the published META-INF/NOTICE of each language artifact and of compose-syntax-highlight-core.
Compose Multiplatform syntax highlighter for Android (and any JVM-based Compose target). Built on tree-sitter for accurate, fast highlighting — with an incremental engine for editor scenarios.
This is still an experimental project. Android is supported via the published AAR; the JVM artifact ships for Compose Desktop interop; iOS is in preview (iosArm64 klibs only — Apple Silicon devices; simulator publishing waits on the next ktreesitter release). For browser apps prefer a JS-side highlighter (e.g. highlight.js, Shiki) and keep this library on JVM-based targets.
API documentation: https://mataku.github.io/ComposeSyntaxHighlighter/ (latest release)
Read-only viewer (:material3) |
Text field (:material3-text-field) |
|---|---|
![]() |
| Platform | Status | Notes |
|---|---|---|
| Android | available | minSdk 26 |
| Desktop JVM | available | KTreeSitter native lib must be on java.library.path
|
| iOS | preview | iosArm64 klibs; device only (simulator blocked on ktreesitter) |
| Web (wasmJs) | not in scope | use highlight.js / Shiki on the JS side |
| Module | Artifact | Latest |
|---|---|---|
:core |
compose-syntax-highlight-core |
|
:material3 |
compose-syntax-highlight-material3 |
|
:material3-text-field |
compose-syntax-highlight-material3-text-field |
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}Add a Material binding plus whichever language modules you need:
// build.gradle.kts (commonMain)
// Material3 binding — ships SyntaxHighlightedText. Pulls in :core transitively.
implementation("io.github.mataku:compose-syntax-highlight-material3:$latestVersion")
// Language artifacts — one per language you want to highlight.
implementation("io.github.mataku:compose-syntax-highlight-kotlin:$latestHighlightKotlinVersion")
implementation("io.github.mataku:compose-syntax-highlight-swift:$latestHighlightSwiftVersion")For an editable code surface, depend on :material3-text-field instead of (or alongside) :material3 — it ships SyntaxHighlightedTextField:
implementation("io.github.mataku:compose-syntax-highlight-material3-text-field:$latestVersion")Each language module is published with its own version, independent from the
core stack. When the versions on your classpath are incompatible — for example,
upgrading the core stack across a major bump without also upgrading language
modules — Gradle reports a strictly conflict on
compose-syntax-highlight-api. See
docs/compatibility.md for the matrix of compatible
versions per artefact.
If you build your own Text on top of the produced AnnotatedString, or you only need highlight() to feed an existing UI, depend on :core directly:
implementation("io.github.mataku:compose-syntax-highlight-core:$latestVersion")
implementation("io.github.mataku:compose-syntax-highlight-kotlin:$latestHighlightKotlinVersion"):core ships highlight(), rememberHighlightedString(), the SyntaxTheme data class, and LocalSyntaxTheme, with only compose.runtime and compose.ui as dependencies.
import io.github.mataku.compose.highlight.api.Languages
import io.github.mataku.compose.highlight.kotlin.kotlin
import io.github.mataku.compose.highlight.material3.SyntaxHighlightedText
@Composable
fun MyScreen() {
SyntaxHighlightedText(
code = """
fun main() {
println("hello")
}
""".trimIndent(),
language = Languages.Kotlin,
)
}The default theme is SyntaxTheme.DarkDefault. To override it for a single call, pass theme directly:
SyntaxHighlightedText(
code = code,
language = Languages.Kotlin,
theme = SyntaxTheme.LightDefault
)Or override for an entire subtree with CompositionLocalProvider:
import io.github.mataku.compose.highlight.core.LocalSyntaxTheme
import io.github.mataku.compose.highlight.core.SyntaxTheme
CompositionLocalProvider(LocalSyntaxTheme provides SyntaxTheme.LightDefault) {
SyntaxHighlightedText(code = code, language = Languages.Kotlin)
}SyntaxHighlightedText paints theme.background behind the text when it is set (every built-in theme defines one). Layering, from outside in, is modifier → background → contentPadding → text, so use modifier for outer margin and contentPadding for the inset between the background edge and the text:
SyntaxHighlightedText(
code = code,
language = Languages.Kotlin,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp), // outside the background
contentPadding = PaddingValues(16.dp), // inside the background
)To suppress the background and paint your own container, override the theme:
SyntaxHighlightedText(
code = code,
language = Languages.Kotlin,
theme = SyntaxTheme.DarkDefault.copy(background = null),
)SyntaxHighlightedText wraps its output in a SelectionContainer by default, so users can highlight the rendered code with the platform's native text-selection UI (which also surfaces a Copy action on Android and Desktop). Pass selectable = false to render without a selection scope:
SyntaxHighlightedText(
code = code,
language = Languages.Kotlin,
selectable = false,
)For an editable syntax-highlighted text surface, use SyntaxHighlightedTextField from :material3-text-field:
val state = remember { TextFieldState(initialText = "val greeting = \"Hello\"") }
SyntaxHighlightedTextField(
state = state,
language = Languages.Kotlin,
theme = SyntaxTheme.DarkDefault,
modifier = Modifier.fillMaxSize(),
)The Composable is backed by IncrementalHighlighter from :core: typing
recomputes only the edited byte range, so highlighting stays interactive on
multi-thousand-line files. For non-Material3 chrome or custom layouts, use
rememberSyntaxHighlightedString directly:
val highlighted = rememberSyntaxHighlightedString(state, Languages.Kotlin)
// drop `highlighted.value` into your own overlay / Text composition.All themes are static SyntaxTheme values on SyntaxTheme.Companion, shipped in compose-syntax-highlight-core. Attributions for the third-party themes are bundled in the artifact's META-INF/NOTICE.
| Theme | Variant | Inspired by / source |
|---|---|---|
SyntaxTheme.DarkDefault |
dark | VSCode-inspired neutral palette (default for LocalSyntaxTheme) |
SyntaxTheme.LightDefault |
light | VSCode-inspired neutral palette |
SyntaxTheme.SolarizedDark |
dark | Solarized by Ethan Schoonover |
SyntaxTheme.SolarizedLight |
light | Solarized by Ethan Schoonover |
SyntaxTheme.GitHubDark |
dark | GitHub Primer syntax tokens |
SyntaxTheme.GitHubLight |
light | GitHub Primer syntax tokens |
SyntaxTheme.OneDark |
dark | Atom One Dark (one-dark-syntax) |
SyntaxTheme.OneLight |
light | Atom One Light (one-light-syntax) |
SyntaxTheme.Dracula |
dark only | Dracula (no canonical light variant) |
Each capture has its own named field, so the IDE autocompletes them — no string keys to remember:
val myTheme = SyntaxTheme(
baseStyle = SpanStyle(color = Color.White),
keyword = SpanStyle(color = Color.Magenta, fontWeight = FontWeight.Bold),
string = SpanStyle(color = Color.Yellow),
// ...
)Unset fields fall back to a parent prefix — setting keyword covers keyword.return, keyword.function, etc., and string.escape falls back to string when stringEscape is unset. For grammar-specific captures not covered by a field (e.g. keyword.return, variable.member), use extras:
val myTheme = SyntaxTheme(
keyword = SpanStyle(color = Color.Magenta),
extras = mapOf("keyword.return" to SpanStyle(color = Color.Red)),
)extras wins over typed fields for the same name.
SyntaxHighlightedText is synchronous by default — fine up to a few
hundred lines. For larger inputs, opt into the async path or
IncrementalHighlighter; see docs/performance.md
for behaviour, code examples, and benchmark methodology.
full highlight medians (warm), Kotlin:
| Runtime | 100 lines | 1k lines | 5k lines |
|---|---|---|---|
| Host JVM (heap 2g) | 5.0 ms | 48.8 ms | 239.8 ms |
| Android (BenchmarkRule, FTL) | 3.1 ms | 30.3 ms | 143.1 ms¹ |
Environment:
¹ Flagship-class only — see docs/performance.md.
Realistic code samples (~75–150 lines, single machine). First use includes one-time tree-sitter query compilation; subsequent use reflects normal app cost.
| Language | Lines | Min (ms) | Median (ms) | Max (ms) |
|---|---|---|---|---|
| Kotlin | 100 | 1083.737 | 1150.319 | 1176.068 |
| Swift | 100 | 728.042 | 751.514 | 760.699 |
| Ruby | 100 | 397.201 | 412.628 | 440.905 |
| Rust | 100 | 391.256 | 411.147 | 412.385 |
| Python | 100 | 370.287 | 372.377 | 467.543 |
| Go | 100 | 320.408 | 325.528 | 355.918 |
| Java | 100 | 341.338 | 362.281 | 408.819 |
| Language | Lines | Min (ms) | Median (ms) | Max (ms) |
|---|---|---|---|---|
| Kotlin | 100 | 4.707 | 4.959 | 5.053 |
| Swift | 100 | 3.539 | 3.716 | 3.775 |
| Ruby | 100 | 3.424 | 3.505 | 3.511 |
| Rust | 100 | 3.356 | 3.437 | 3.452 |
| Python | 100 | 3.390 | 3.471 | 3.474 |
| Go | 100 | 2.341 | 2.346 | 2.353 |
| Java | 100 | 3.040 | 3.153 | 3.205 |
Environment: Apple M3 Pro, OpenJDK 21, heap 2g, warmup 100 / measure 50.
Full config and methodology: docs/performance.md.
Run ./gradlew :benchmarks:jvm:jvmTest to reproduce on your own machine.
MIT. Bundled grammars are all MIT-licensed:
The built-in SyntaxTheme palettes shipped in compose-syntax-highlight-core derive their colors from the following third-party themes (all MIT-licensed):
Full attributions are in the published META-INF/NOTICE of each language artifact and of compose-syntax-highlight-core.