Library / SDK
jeziellago/compose-markdown avatar
jeziellago/compose-markdown

compose-markdown puts a TextView renderer behind one Compose composable

Markdown Text for Android Jetpack Compose 📋.

873 stars84 forksKotlinMIT

At a glance

What is it?
Compose Markdown is a Kotlin library that renders Markdown inside Jetpack Compose with a single MarkdownText composable, and it works by wrapping a TextView based renderer rather than by drawing text on a Canvas. That decision is what makes the styling options familiar, and it is also what a reviewer should understand before adopting it.
Who is it for?
compose-markdown fits an Android app that already ships Compose and needs to display Markdown from a server, a changelog or a help page without building a parsing pipeline, and that is comfortable with a View backed renderer inside its Compose tree. Skip it if your team needs the rendered output to be a first class Compose text layout, or if you cannot accept a View hierarchy element inside a Compose screen.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 17 days ago.
What is it written in?
Mainly Kotlin, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The renderer is a TextView, not a Compose text layout

The README is upfront about the architecture in its second sentence: this library wraps a `TextView`-based renderer in Compose so you can display rich Markdown content while still using familiar Compose modifiers and text styling.

That single sentence tells you most of what you need to know about the trade-offs. The Markdown is parsed and laid out by Android's classic View text stack, and that View is hosted inside the Compose tree. You get `Modifier.padding`, `TextStyle`, `maxLines` and the rest because the composable accepts them and applies them to the hosted View, not because Compose is doing the text layout.

The upside is coverage. CommonMark-style rendering, HTML support, tables, task lists, remote images and GIFs, linkification of URLs, emails and phone numbers, and `==highlight==` spans are all listed features, and supporting them from scratch in a Compose text pipeline is a serious piece of work. Hosting a TextView gets that behaviour without writing a parser.

The downside is that the composable is not a pure Compose citizen, and the API surface reflects that. Options such as `autoSizeConfig`, which enables auto-sizing on API 26 and above, `isTextSelectable` for long-press selection, and `onTextLayout` for observing the rendered line count are all things the View text stack provides, and they do not map one to one onto the equivalent Compose text APIs. Any composable that expects to measure or lay out Compose text will not find it here.

Installing from JitPack and rendering your first block

The dependency is distributed through JitPack rather than Maven Central, so the first step is adding the JitPack repository to your root `settings.gradle`.

groovy
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven { url 'https://jitpack.io' }
    }
}

Then the dependency itself goes into your module's `dependencies` block, with the version placeholder the README uses.

groovy
dependencies {
    implementation 'com.github.jeziellago:compose-markdown:VERSION'
}

The artifact coordinates carry the JitPack `com.github.<owner>` prefix because the build is produced from the repository on demand, and the library module itself lives in the `markdowntext/` directory of the tree, with a `sample/` app that demonstrates what it can render.

Using it is then a matter of two imports and one call.

kotlin
import androidx.compose.runtime.Composable
import dev.jeziellago.compose.markdowntext.MarkdownText
kotlin
@Composable
fun ArticleBody() {
    val markdown = """
        # Compose Markdown

        images, tables, task lists, and even inline HTML.

        - [x] Markdown
        - [x] HTML
        - [x] Images
    """.trimIndent()

    MarkdownText(markdown = markdown)
}

The package name is `dev.jeziellago.compose.markdowntext`, and the composable takes the Markdown as a string, so anything you can produce as text can be rendered, including content fetched at runtime.

The option list is where the real surface is

The README devotes a section to the parameters `MarkdownText` exposes, and that list is the closest thing to an API reference the project has.

text
- `modifier`: apply Compose layout and interaction modifiers
- `style`: configure color, alignment, font size, line height, and more
- `maxLines`: limit rendered lines
- `truncateOnTextOverflow`: enable ellipsizing
- `fontResource`: apply an Android font resource
- `isTextSelectable`: enable long-press text selection
- `onTextLayout`: observe rendered line count

A few of these are worth calling out. `fontResource` applies an Android font resource, so typefaces come from the resource system rather than from a Compose `FontFamily`. `truncateOnTextOverflow` with `maxLines` gives you a preview card that ellipsizes rather than rendering an entire document. `onTextLayout` reports the rendered line count, which is how you would implement a read more affordance.

The styling example in the README shows the same controls used the way you would use them in Compose, with a `TextStyle` carrying a color, a font size, a line height and a text alignment, and a `Modifier.padding` applied to the composable. The composition of a Compose `TextStyle` and a View backed renderer is the interesting part of the API, and it is where you should look when something does not look the way you expect.

The list continues with options for images and links, including `imageLoader` to provide a custom Coil `ImageLoader`, `linkColor` to override the link color, and `enableUnderlineForLink` to turn link underlines on or off. Since remote images and GIFs are supported, supplying your own image loader is how you control caching and request behaviour for content that comes from a server.

Clicks on the block and clicks on a link are separate callbacks

Interaction is where the composable is most opinionated, and the two axes are kept apart on purpose.

`onClick` handles clicks on the whole composable, and `onLinkClicked` intercepts markdown link clicks, with the URL passed to the callback. The advanced example in the README wires both to a toast, so a reader can see the two paths side by side.

kotlin
        style = TextStyle(fontSize = 18.sp),
        isTextSelectable = true,
        onClick = {
            Toast.makeText(context, "Block clicked", Toast.LENGTH_SHORT).show()
        },
        onLinkClicked = { url ->
            Toast.makeText(context, url, Toast.LENGTH_SHORT).show()
        },

There is a third option that resolves the conflict. `disableLinkMovementMethod` disables the internal link handling so that parent click handlers can receive taps, which is what you need when the whole block sits inside something clickable, such as a list row.

That combination is worth thinking about. With `onClick` on the block and `onLinkClicked` on links, a tap on a link inside a clickable block has two possible destinations, and the resolution is a configuration choice rather than something the library decides for you. A team shipping Markdown inside tappable cards needs to be explicit about it, and the README does not document the precedence, so the behaviour is worth confirming in your own build.

TalkBack sees one node until you ask for blocks

Accessibility gets a dedicated section in the README, and the default is the part worth knowing about.

By default, TalkBack announces the entire Markdown content as a single node. Setting `enableBlockLevelAccessibility` exposes individual text blocks, paragraphs, lists and code blocks among them, as separate accessibility nodes, so a screen reader user can move to a specific block rather than hearing the whole document as one utterance.

kotlin
        enableBlockLevelAccessibility = false  // TalkBack sees one big text node

The option defaults to false, so a team shipping long-form content to Android users with screen readers gets the poor experience unless someone turns it on. The README frames the default as the behaviour and the flag as the opt-in, and the flag is listed among the useful options with its default spelled out, which is more than most libraries document.

This is a real limitation of a View based renderer in a Compose world. The accessibility tree is the View tree, so exposing Compose style blocks means synthesising virtual accessibility nodes for the text blocks, and the flag name says that, calling them virtual. The default of false is a performance and compatibility choice, and it means accessibility is opt-in rather than the default state of a component that renders arbitrary documents.

Markdown scope, and what the README leaves unsaid about untrusted content

The list of supported content is long and specific: headings, paragraphs, bold, italic and strikethrough text, inline code and fenced code blocks, blockquotes, ordered and unordered lists, horizontal rules, tables, HTML content, remote images and animated GIFs, task lists, autolinked URLs, emails and phone numbers, and highlighted text with `==highlight==`.

Two of those deserve attention from a security point of view. HTML content is rendered, and remote images are fetched, which means a Markdown document from an untrusted source can carry both markup and network fetches. The README does not document how HTML is sanitised, what tags are allowed, or how remote image requests are authenticated and cached, and it does not say what happens to a link that points at a local address.

That silence is a gap rather than a fault. The library is a renderer, and the usual arrangement is that the application decides what it will load. But it means the security review of a feature that renders server-side Markdown cannot stop at the dependency, because the answer to several of those questions is in the source of the wrapped renderer and in the application code around it.

The same applies to the custom Coil `ImageLoader` option. Supplying your own loader is the documented way to control how remote images are fetched, and for a document that can come from anywhere, that is the setting to review first.

Version 0.7.x, a JitPack dependency, and MIT licensing

The release history is short. Version 0.7.1 was published on 2026-04-07, 0.7.2 on 2026-04-27 and 0.7.3 on 2026-10-08, and the last push was on 2026-10-08 as well, so the most recent release matches the last commit rather than trailing it.

The project is under a 0.x version, which means the API can still move between patch releases, and the dependency coordinate carries a version placeholder in the README rather than a pinned number, so the version choice is yours to make.

The distribution detail is the one that shapes day to day work. Because the library is built on demand by JitPack from the repository, the first resolution of a new version in a clean build is a remote build, which is slower than pulling a published artifact and which can fail when the upstream build environment has an issue. A team with a strict build reproducibility policy has to account for that, and a team that wants a fixed binary should consider vendoring the module or mirroring the artifact.

The licence is MIT, and the licence text sits in the `LICENSE` file at the top of the tree, so there is no obligation beyond attribution when you ship it in an application. The repository also carries the standard build files for an Android library, a `build.gradle`, `gradle.properties`, a `settings.gradle` and the Gradle wrapper, plus a `jitpack.yml` that configures the on demand build.

Editorial conclusion

compose-markdown fits an Android app that already ships Compose and needs to display Markdown from a server, a changelog or a help page without building a parsing pipeline, and that is comfortable with a View backed renderer inside its Compose tree. Skip it if your team needs the rendered output to be a first class Compose text layout, or if you cannot accept a View hierarchy element inside a Compose screen. Verify first that the version you pick from JitPack matches your Compose toolchain, and read how HTML and remote images are handled before you feed it untrusted content.

Frequently asked questions

How do I add compose-markdown to an Android project?

Add the JitPack repository to your root `settings.gradle`, then add the dependency `com.github.jeziellago:compose-markdown:VERSION` to your module. The README's install steps show the repository block with `maven { url 'https://jitpack.io' }` and the dependency block with the `implementation` configuration.

How is compose-markdown rendered inside Compose?

The README states that the library wraps a `TextView`-based renderer in Compose, so the Markdown is rendered by the Android View text stack and hosted inside the Compose tree. Styling options such as `style`, `maxLines` and `fontResource` are applied on top of that hosted view.

Does MarkdownText support links, images and tables?

Yes. The supported content list includes tables, HTML content, remote images and animated GIFs, task lists, autolinked URLs, emails and phone numbers, and `==highlight==` highlighted text. Remote images can use a custom Coil `ImageLoader` through the `imageLoader` option.

How do I make the Markdown accessible to TalkBack?

Set `enableBlockLevelAccessibility` to true, which exposes individual text blocks such as paragraphs, lists and code blocks as separate accessibility nodes. The default is false, and by default TalkBack announces the whole Markdown content as a single node.

How do I handle a click on a link inside a tappable Markdown block?

The composable separates the two cases: `onClick` handles clicks on the whole composable and `onLinkClicked` intercepts markdown link clicks with the URL. The `disableLinkMovementMethod` option disables the internal link handling so that parent click handlers can receive taps.

Official sources

  1. Issues
  2. jeziellago/compose-markdown on GitHub
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/jeziellago-compose-markdown.svg)](https://hysenlabs.com/projects/jeziellago-compose-markdown)

Was this useful?