Thesis
Why Undra keeps your UI native
Cross-platform tools fall into two camps. One shares the pixels: you write the UI once and a toolkit puts it on every platform. The other shares what is under the pixels and lets each platform draw its own screens. Undra is in the second camp, and this post explains why without pretending the first camp is wrong. Shared UI is the right answer for many products, and we say which.
Two bets on going cross-platform
The first bet is that the UI is the expensive part, so share it. Three mature tools make this bet in different ways.
- Flutter has its own rendering engine, Impeller, and its own implementations of each UI control rather than deferring to the system's (architecture overview). You write Dart, and Flutter paints its own widgets with its own engine.
- React Native turns a tree of React components into the platform's own views: a
UIViewon iOS, anandroid.view.ViewGrouporandroid.widget.TextViewon Android (render pipeline). You write JavaScript or TypeScript and the screen is made of host views. Its New Architecture has been the default since React Native 0.76 (docs). - Compose Multiplatform takes Jetpack Compose to other platforms. JetBrains lists iOS as Stable and web as Beta and describes interop that lets you keep existing SwiftUI or Android View code (product page). Its FAQ says the components are drawn on a canvas rather than being platform widgets (FAQ).
The second bet is that the logic is the expensive part, so share that and keep the UI native. Undra, UniFFI, Crux and Kotlin Multiplatform used without Compose Multiplatform all make it, with different amounts of framework around the shared code. The Kotlin Multiplatform comparison and the UniFFI and Crux comparison cover the differences among them.
The line under the pixels
Undra draws the line exactly at the UI. Everything under it is shared: domain logic, reactive state, the data layer, persistence and the dev loop. Everything above it stays a hand-written SwiftUI, Jetpack Compose or React screen.
Our reasoning starts from where the work repeats. In our experience the code beneath the UI is where the three platforms disagree and where the subtle bugs live: the cache, the retry policy, the offline queue, pagination, the session refresh. Teams write these three times and get three slightly different answers. The screens, by contrast, are where platform expertise pays off. So Undra shares the part that must be identical and leaves the part that should feel native.
The bar is that a SwiftUI view bound to Undra state is indistinguishable from one bound to Swift state. That is achievable because the generated store is an ordinary @Observable class, a Kotlin StateFlow or a TypeScript Signal. The same store in the playground, in each platform's own UI code:
Playground code, abbreviated: examples/playground/ios/PlaygroundApp/Screens/TodosScreen.swift
ForEach(todos.visible, id: \.id) { todo in
Button {
todos.toggle(id: todo.id)
} label: {
Label(todo.title, systemImage: todo.done ? "checkmark.circle.fill" : "circle")
}
// ...
}
Playground code, abbreviated: examples/playground/android/.../ui/TodosScreen.kt
val visible by store.visible.collectAsState()
val remaining by store.remaining.collectAsState()
Playground code, abbreviated: examples/playground/web/src/views/TodosView.tsx
const visible = useSignal(todos.visible);
const remaining = useSignal(todos.remaining);
Three screens, each written in its platform's idiom, reading one core's state. The consequence for day-one features is direct: because the UI is plain SwiftUI, Compose or React, a new platform API is available to you on the day it ships. Accessibility modifiers, previews, navigation, animation and the design system are the platform's own and stay yours. Undra's part is to keep what those screens show consistent with the logic, and it does that with a local mirror of the state, so reads from the UI never cross into the core.
Native tooling applies unchanged for the same reason. The playground's iOS screens carry accessibility identifiers and are driven by an XCUITest tour against the real core; its Compose screens use test tags; its web build is exercised by a Playwright smoke test in headless Chromium. Nothing in those tests knows Undra exists, because what they see is a SwiftUI, Compose or React UI.
What the line costs
Drawing the line here has costs, and they are the mirror image of what shared UI buys.
- You build three UIs. That means three implementations of the design, three UI test suites, and keeping the screens from drifting apart. If your product needs pixel-identical screens, this is the wrong tool.
- You need Rust. The core is Rust. Undra's macros emit errors that say what, why, how to fix it and where to read more, and they keep lifetimes and threads off the common path, but the borrow checker remains, and in our experience most mobile teams have few Rust engineers. Only the people writing the core need it; the platform teams consume generated Swift, Kotlin and TypeScript.
- Logic must be worth sharing. If your app is mostly UI with little logic beneath it, sharing the logic buys little. The worst case for Undra is an app that is 90 percent UI and 10 percent logic.
- It is a new framework. Undra 1.0 shipped on 30 September 2026. Its reference app is the playground in the repository, and its evidence is its tests: more than 4,000 across five languages, and 18 contract scenarios run on iOS, Android and web. We are not claiming production mileage.
- The scope is a choice. Undra v1 has no sync engine, no hosted services and no shared UI. The last is a decision and not a backlog item.
Choosing
| Your situation | A better fit |
|---|---|
| Small team, one UI codebase, brand-led custom interface | Shared UI: Flutter, React Native or Compose Multiplatform |
| An existing React team that wants mobile apps | React Native |
| A Kotlin team, Android first, wanting iOS screens too | Compose Multiplatform, which JetBrains lists as Stable on iOS |
| Existing native iOS and Android apps with platform teams and duplicated logic | Shared logic: Undra, Kotlin Multiplatform, UniFFI or Crux |
| Platform-idiomatic UI is a product requirement | Native UI with shared logic |
| A Rust library already exists and you need bindings | UniFFI |
| An app that is mostly UI with thin logic | Shared UI |
| Stateful, server-backed logic on three platforms that must stay consistent | Undra |
The two bets are not exclusive over time. Some teams share logic first and UI later, and a Kotlin team can do that inside one ecosystem. We have not built or tested Undra underneath a shared-UI toolkit, and Undra's generated bindings target SwiftUI, Compose and React idioms, so we make no claim there.
If the argument persuades you, the next question is how the state model works. Reads never cross the boundary explains it in detail, and the getting started guide gets a three-platform app running.