LlmTypewriter
The streaming-text typewriter built for LLM apps on Compose Multiplatform. Renders a
Flow<String> of tokens with live progressive Markdown, syntax-highlighted code blocks that
build up as tokens arrive, three speed curves (linear / ease-out / natural), a configurable
blinking cursor, tap-to-skip, graceful stop-mid-stream, selectable text, and a screen-reader-
friendly live region — on every CMP target.

Pairs naturally with prompt-bar — drop both in and you have a ChatGPT-quality chat UI on Android, iOS, Desktop, and Web in ~20 lines. PromptBar's Send/Stop button auto-syncs with state.isStreaming.
Why another typewriter?
Existing CMP typewriters take a static String — they're animations, not stream renderers.
LlmTypewriter is built around the AI chatbot streaming-token use case that nobody else has
shipped:
Install
dependencies {
implementation("io.github.nadeemiqbal:llm-typewriter:0.1.0")
}
Usage
Stream an LLM response
@Composable
fun ChatBubble(chatViewModel: ChatViewModel) {
val state = rememberStreamingTypewriterState()
StreamingTypewriter(
tokens = chatViewModel.responseFlow,
state = state,
renderer = rememberMarkdownTypewriterRenderer(),
cursor = TypewriterCursor.Line,
speedCurve = SpeedCurve.Natural,
)
Button(onClick = { state.stop() }, enabled = state.isStreaming) { Text("Stop") }
}
Static text — banner / hero
TypewriterText(
text = "Build LLM-powered apps for Android, iOS, Desktop, and Web.",
cursor = TypewriterCursor.Block,
speedCurve = SpeedCurve.Natural,
)
Cycling banner
CyclingTypewriterText(
phrases = listOf("Type", "Stream", "Render", "Anywhere"),
holdMs = 1200L,
)
Custom cursor
StreamingTypewriter(
tokens = tokens,
cursor = TypewriterCursor.Custom {
Box(Modifier.size(12.dp).background(MaterialTheme.colorScheme.primary, CircleShape))
},
)
Speed curves
Renderers
| Renderer | What it does |
|---|
PlainTypewriterRenderer | Paints raw text in the ambient style. Zero parsing cost. |
rememberMarkdownTypewriterRenderer() |
The Markdown parser is prefix-stable: a **bold mid-stream renders as plain text until the
closing ** arrives, but every token before that opening ** stays exactly where it was.
Code fences highlight progressively as tokens stream in — no waiting for the closing ```.
Platforms
| Target | Status |
|---|
| Android (minSdk 24) | ✅ |
| iOS (x64, arm64, simulatorArm64) | ✅ |
State API (headless)
StreamingTypewriterState is fully usable without composition — handy for tests and for hosts
that want to drive the typewriter from a background coroutine:
val state = StreamingTypewriterState()
state.appendToken("Hello, ")
state.appendToken("world!")
state.completeSource()
state.skipToEnd()
state.stop()
state.resume()
state.reset()
Sample app
./gradlew :sample:desktopApp:run
./gradlew :sample:androidApp:assembleDebug
./gradlew :sample:webApp:wasmJsBrowserDevelopmentRun
The sample includes a fake-LLM that streams pre-canned responses with Markdown + code blocks, a
speed-curve switcher, and Stop/Resume/Skip controls.
License
Apache 2.0 — see LICENSE.