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 UIView on iOS, an android.view.ViewGroup or android.widget.TextView on 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.

What shared UI buys

The benefits are large and we do not want to undersell them.

  • One UI codebase. One set of screens, one set of UI tests, one design system implementation. A small team ships to several platforms with the headcount of one.
  • Consistency by construction. The same screen behaves the same way everywhere, which matters when the product's identity is its own look and not the platform's.
  • Iteration speed. These toolkits put a lot of effort into fast feedback loops for UI work, and a single codebase means a fix lands everywhere at once.
  • Fit with your existing skills. A team that already writes React can reach mobile through React Native. A Kotlin team can reach iOS through Compose Multiplatform.

Shared UI is usually the right choice when any of these describes the product:

  • A small team or a startup that needs to reach several platforms quickly.
  • A brand-led or highly custom interface, where a bespoke look matters more than platform convention.
  • An internal tool or a content app where the screens are simple and the platform's idioms are not a selling point.
  • An app that is mostly UI with a thin logic layer, where there is little below the pixels worth sharing.

If that is you, use one of those toolkits. Undra will not help, and we do not plan to add shared UI.

What shared UI trades away

The trade is not the same for each toolkit, and none of it is fatal. It is a set of costs that show up at the edges, and the costs are different if you start from a native app than if you start from nothing.

Platform idioms. Native controls bring platform behaviour with them: text selection, scroll physics, navigation transitions, keyboard handling, system sheets and menus. React Native inherits much of this because its screens are host views. Flutter and Compose Multiplatform draw their own controls, so matching a platform convention is the toolkit's work, or yours. Brand-led apps often do not want a match, which is the point of the first bet.

Day-one OS features. When a platform ships a new UI API, native code can adopt it with the new SDK. A shared toolkit adopts it on its own schedule, or you embed a native view. Flutter documents platform views for exactly this, and notes that synchronising them with Flutter's rendering carries overhead, so it recommends them for complex controls where reimplementing is impractical. Some surfaces are built in the platform's own UI framework regardless: iOS home-screen widgets, for example, are WidgetKit views written in SwiftUI (Apple), so a shared-UI app writes those natively anyway.

Accessibility fidelity. With native UI, the platform's accessibility stack talks to platform views directly. A toolkit that draws its own controls must publish a semantics tree that the platform accessibility APIs can read. Flutter documents a semantics tree, built in its own pipeline phase and uploaded to the engine for assistive technology (docs), and JetBrains documents that Compose semantics are mapped to native iOS accessibility objects and support VoiceOver (docs). These work and are widely used. The cost is that keeping parity with each platform's accessibility behaviour is the toolkit's ongoing job, where with native UI it is the platform's.

The hiring pool. Platform teams hire SwiftUI, Compose and React specialists. A shared-UI toolkit has its own pool (Dart, React Native, Kotlin with Compose), and each of those is large. The cost appears when you already employ platform specialists: moving to shared UI re-skills or replaces them. Undra's version of this problem is Rust, which we come back to below.

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.

Why not share the UI too?

We considered it and rejected it, for a reason that is about focus more than principle. Flutter, React Native and Compose Multiplatform are whole-UI products, each maintained by a large team that chases every platform's new UI features, keeps accessibility in step and polishes rendering. An Undra UI toolkit would be a smaller and later version of one of them, and it would be the part of Undra most likely to age badly. The moment a framework paints pixels on three platforms it takes on that ongoing work, and it inherits the trade-offs listed above.

There is also a practical reason. If the UI stays native, Undra's generated code has a clear bar to meet: a Swift, Kotlin or TypeScript engineer who has never seen Rust must be able to review it and find it idiomatic. The golden files that lock the generated output are how the project holds itself to that bar.

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 situationA better fit
Small team, one UI codebase, brand-led custom interfaceShared UI: Flutter, React Native or Compose Multiplatform
An existing React team that wants mobile appsReact Native
A Kotlin team, Android first, wanting iOS screens tooCompose Multiplatform, which JetBrains lists as Stable on iOS
Existing native iOS and Android apps with platform teams and duplicated logicShared logic: Undra, Kotlin Multiplatform, UniFFI or Crux
Platform-idiomatic UI is a product requirementNative UI with shared logic
A Rust library already exists and you need bindingsUniFFI
An app that is mostly UI with thin logicShared UI
Stateful, server-backed logic on three platforms that must stay consistentUndra

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.