Back to insights
Insight

Choosing Tamagui for Cross-Platform Work

A project this summer needed to ship on web and mobile without maintaining two design systems. Why I chose Tamagui, what its compiler actually buys you, and where I would still hesitate.

One machined component seated into two different housings, with the shared piece picked out in Persimmon

A project I picked up this summer needed to exist as a web application and as mobile apps. Not a website with a wrapper around it, but real applications on both, sharing a design language and, ideally, most of their interface code.

That requirement narrows the field quickly, and it is where I ended up choosing Tamagui.

The problem is styling, not components

Sharing logic across React and React Native has been manageable for years. Hooks, state, data fetching, and validation all move without much friction. The part that resists is styling.

React Native has no CSS. The web has little else. Most cross-platform attempts resolve that by picking a side: either you ship a runtime style system to native and accept the performance cost, or you write the interface twice and accept the maintenance cost. Neither is satisfying on a project where the interface is the product.

What made Tamagui interesting to me is that it treats this as a compilation problem rather than a runtime one.

What the compiler actually does

Tamagui's optimizing compiler analyzes components at build time and does work that would otherwise happen on every render: partial evaluation, tree flattening, hoisting, and dead-code elimination.

In practice a styled component with static props can be flattened into a plain View or div, with the styling extracted to atomic CSS on the web and to a resolved style object on native. Responsive props compile to real CSS rules rather than JavaScript measuring the viewport.

The distinction that matters architecturally: you write one component, and the cost of the abstraction is largely paid at build time rather than in every frame the user sees. That is the right place to pay it.

The system is also split sensibly. The style layer stands on its own, the optimizer is a separate build-time concern with optional plugins for Metro, Vite, and Webpack, and the component kit sits on top. Importantly, it runs without the plugins at all: you can adopt it first and turn the optimizer on later, which lowered the risk of the decision considerably. If you already have a design system, you can take the style layer and skip the components entirely.

Timing this against the New Architecture

This is not a neutral moment to be choosing a cross-platform styling layer.

React Native's New Architecture is mid-migration. Expo SDK 53 makes it the default for new projects, and the direction of travel is clear enough that anything adopted now needs to survive it. Some of the edges are still sharp. I have seen enough issue threads this year to treat the combination of a new SDK, React 19, and the New Architecture as something to pin carefully rather than track eagerly.

That context actually strengthened the case for a compiler-based approach. A library whose main job happens at build time has a smaller surface exposed to native runtime changes than one doing heavy work at render time on the native side. It is not immune, but it is a smaller bet.

I pinned versions, kept the optimizer optional at first, and got the application working before reaching for the performance layer.

Themes are the part that scales

The feature I appreciated most after a few weeks was not the components. It was the theme system.

Themes in Tamagui behave like CSS variables: they cascade, they override as they descend, and they compile to CSS so that changing one does not trigger a re-render of the tree beneath it. Sub-themes go all the way down to individual components.

That sounds like a convenience and turns out to be a structural advantage. Light and dark stop being a special case handled by conditionals scattered through the codebase, and become a property of a subtree. A card rendering inside an inverted panel does not need to know it is inverted. It inherits.

Anyone who has maintained a design system where every component carries its own isDark branch will recognize how much accidental complexity that removes.

A clean design that does not impose a product

The thing I keep coming back to is restraint.

A lot of UI kits arrive with a strong opinion about what your product should look like, and adopting one means either accepting that look or fighting it. Tamagui's components are deliberately unopinionated in appearance: they give you behavior, accessibility, and structure, and leave the visual identity to your tokens and themes.

That is why it fits a wide range of projects rather than one house style. The styled() API, with its variants and size scales, means you build your own vocabulary on top rather than overriding someone else's. For consulting work in particular, where each client arrives with an existing brand, that flexibility is worth more than a beautiful default theme I would only have to dismantle.

On the direction of the project

I pay attention to whether a tool's author is building toward something coherent, because that tends to predict how the project ages.

Nate Wienert released Tamagui in 2021, and last year followed it with One, a React framework covering web and native. That sequence reads to me as a consistent thesis rather than feature accumulation: solve styling across the two targets first, then solve the framework layer around it. The compiler-first instinct in Tamagui is the same idea at a smaller scale: do the expensive work once, ahead of time, so the running application stays simple.

One is still early, and I would not build a client project on it today. But the existence of a coherent next step tells me something useful about how the styling layer beneath it is likely to be maintained. I find that more reassuring than a long changelog.

Where I would still hesitate

I would not present this as a default choice for every project.

The compiler is the source of most of the benefit, and it is also the part that adds complexity to your build. If your team has not worked with a build-time style system before, budget time for understanding what the optimizer is doing when something does not flatten the way you expected. Starting with it turned off is a reasonable way to defer that cost.

It is also a smaller ecosystem than the web-only alternatives, and the React Native side is moving underneath everyone right now. If a project is web-only, I would reach for something more conventional. Paying for cross-platform machinery you do not need is its own kind of over-engineering.

But for the case it was built for (one team, one design language, real applications on web and mobile) I think it is the most coherent answer currently available.