I’ve been writing Android UI in Jetpack Compose for a couple of years now. I like it. But there’s one thing about it that always bugged me compared to React: the mental model for conditional rendering.
In React:
{
isLoggedIn && <UserProfile />;
}
{
items.map((item) => <Item key={item.id} {...item} />);
}
In Compose:
if (isLoggedIn) {
UserProfile()
}
items.forEach { item ->
Item(item)
}
It’s not that Kotlin’s version is wrong — it’s perfectly fine Kotlin. But the JSX version maps more naturally to how I think about UI as a function of data. I wanted something in the middle.
ktsx is my experiment at bringing that syntax to Kotlin Multiplatform.
What it does
ktsx introduces .ktsx files that compile to .kt at build time via a Gradle plugin and a KSP (Kotlin Symbol Processing) processor. The syntax looks like this:
<Column modifier={Modifier.fillMaxSize().padding(16.dp)}>
<if condition={isLoggedIn}>
<UserProfile name={user.name} />
</if>
<for each={items} as="item">
<ItemCard title={item.title} />
</for>
<show when={isLoading}>
<CircularProgressIndicator />
</show>
</Column>
That gets compiled to idiomatic Compose Kotlin at build time — no reflection, no runtime overhead.
Why I built it
Partly as a learning project (I wanted to understand how Gradle plugins and KSP processors work), partly because I genuinely wanted to use this syntax. The directives — if, for, show — read clearly in code review and are harder to accidentally misplace than nested Kotlin if blocks inside composables.
The hard parts
Writing the Gradle plugin
I’d never written a Gradle plugin before. The API surface is enormous and the documentation is scattered between Gradle’s own docs, the Kotlin Gradle plugin docs, and a handful of blog posts. Getting the plugin to hook into the Kotlin compilation pipeline at the right phase — before KSP runs, but after source sets are resolved — took a lot of trial and error.
The key insight was using KotlinCompilation.compileTaskProvider.configure to inject a pre-compile transformation task that rewrites .ktsx files to .kt before the Kotlin compiler even sees them.
The parser
I wrote a hand-rolled recursive descent parser for the ktsx syntax. I briefly considered using ANTLR, but it felt like overkill for what’s essentially a simple XML-like syntax with Kotlin expressions in {} attribute positions.
The tricky part is the expression parser inside attributes — {user.name} is straightforward, but {items.filter { it.active }.take(5)} has nested braces that naive bracket-counting would mishandle. I ended up tracking brace depth rather than treating the content as a raw string.
The ComponentRegistry
One design decision I’m still unsure about is the ComponentRegistry — a mechanism that lets you register custom composables so ktsx knows how to output them. It works, but it adds configuration overhead that feels un-Kotlin-like. I’m exploring whether KSP annotations on the composable itself could eliminate the registry.
Why not just use React Native?
It’s the obvious question. JSX syntax, component model, mobile UI — that’s React Native’s whole pitch. So why build ktsx instead of just switching?
You stay in Kotlin
React Native is JavaScript (or TypeScript) running on a JS engine, calling native modules through a bridge. ktsx is Kotlin — compiled to native bytecode, running on the JVM or Kotlin/Native depending on your target. There’s no bridge, no JS thread, no serialisation overhead between your UI logic and the platform.
If your codebase is already Kotlin — your business logic, your data layer, your networking — ktsx lets your UI live in the same language and the same module. Shared types, shared models, shared test infrastructure. With React Native you’re splitting your codebase into two languages whether you want to or not.
You keep Jetpack Compose
ktsx compiles down to Compose. That means you get everything Compose gives you for free: the animation APIs, the state management primitives, remember, LaunchedEffect, the full layout system. You’re not working around a framework — you’re adding syntax sugar on top of one that already works well.
React Native’s rendering has improved significantly with the new architecture (JSI, Fabric), but it still doesn’t map 1:1 to native UI components. Compose does. A LazyColumn in Compose is a RecyclerView under the hood. In React Native, a FlatList goes through an abstraction layer before it reaches the platform. For most apps that doesn’t matter; for scroll-heavy or animation-heavy UIs, it can.
No context switching
This is the practical one. If your team writes Kotlin all day and you reach for React Native, you’re asking everyone to context-switch into a different language, a different build system (Metro bundler alongside Gradle), and a different debugging workflow. ktsx is Gradle-native. It fits into the build you already have.
What React Native is better at
To be fair: if your team is primarily TypeScript engineers, React Native is the right call. The ecosystem is enormous — Expo, React Navigation, a decade of battle-tested libraries. Code sharing with a web frontend is real and works well. And if you want JSX syntax in your mobile app and you don’t already have a Kotlin codebase, there’s no reason to reach for ktsx.
ktsx is for Kotlin teams who want the clarity of JSX-style conditional and list rendering without leaving the platform they’re already on. It’s not a replacement for React Native — it’s an answer to a different question.
What’s next
ktsx is very new — the repository is only a few days old at the time of writing. The core compilation pipeline works, and the directive system (if, else, for, show) is functional. Next up is better error messages (right now a syntax error in a .ktsx file produces a confusing Kotlin compiler error), and then a VS Code extension for syntax highlighting.
If you work in Kotlin Multiplatform and the JSX-style syntax appeals to you, I’d love to hear what you think about the design. The repo is on GitHub.