Skip to content
OTFotf
All posts
Topic guide

One Component, Web and Mobile: Cross-Platform UI That Your Agent Can Read, Extend, and Ship

The hard part of building for web and mobile at once was never the styling — it's shipping a component your agent can read, extend, and keep in sync across both platforms.

You wire up a clean component on the web. A button, a sheet, a date picker — props you like, states you tested, a look that matches the rest of the app. Then the mobile build lands on the roadmap, you ask your agent for the React Native version, and it stalls. There is no twin. So it improvises one: a different prop name here, a slightly different look there, a third interaction model because the library it reached for has its own opinions. Two screens that should be the same component are now two components that drift a little more every sprint.

This is the cross-platform UI problem in 2026, and most of the conversation about it is aimed at the wrong layer. Builders search for a cross-platform UI kit web mobile answer expecting a styling question — utility CSS on native, or a new universal renderer. But the styling question has been answered for a while now. The part that still breaks is the contract: the same component, with the same props and the same output, on web and on mobile, described in a way an AI agent can actually pick up and extend without guessing.

This guide walks through that — the one-API architecture, the "shadcn for React Native" gap and why it persisted, the 2026 universal-component landscape, and the dimension almost nobody ships: whether your agent can read the component at all.

The problem: your web component has no mobile twin

Why agents stall at the web-to-native boundary

A coding agent is good at pattern-matching against what it can see. Point it at a web component and ask for a mobile version, and it does the reasonable thing: it looks for an equivalent. When the equivalent lives in a different library with a different API — onClick on one side, onPress on the other; a variant prop here, a kind prop there — the agent can't reconcile them. It can't reason about a component that's split across two unrelated sources with no shared contract between them. So it writes a fresh implementation, and now you own two.

That is the actual failure mode behind "my agent can't find the React Native version of this." It isn't that the agent is weak. It's that there's nothing coherent for it to read. The web component and the mobile component were never the same thing, so the agent treats them as two problems and produces two answers.

The "shadcn for React Native" gap — why it matters and why it persisted

If you search shadcn for React Native, you'll find that the ecosystem mostly has solved the surface problem. There are now solid ways to get shadcn-shaped, accessible components onto a phone, and there are tools that compile web-style utility CSS straight to native styles. You can put the components on both platforms.

What you can't easily do is put the same component on both platforms — one prop contract, one set of states, released together. As the shadcn-for-React-Native gap lays out, the standard approach gives you two registries: a web one and a native one, each with its own upstream, its own conventions, and no shared API guarantee between them. Same name, two files, two upstreams, no contract. For a human, that's a synchronization tax you pay by hand. For an agent, it's worse — it's two orphaned implementations the agent has to keep aligned with no source of truth telling it what "aligned" means.

That gap persisted because solving the easy half (get components onto native) is satisfying and shippable, and solving the hard half (guarantee one contract across both) is invisible until you're three months into maintenance and your two buttons have quietly diverged. The interesting opportunity here, and the reason "shadcn for React Native" is still a live search with thin authoritative coverage, is that almost everyone stopped at the easy half.

What "universal" actually means (and what it doesn't)

"Universal" gets used two ways, and they're not the same promise.

The weak version is "write once, render everywhere" — one codebase that gets squeezed onto every platform, usually by running web rendering primitives on a phone. It demos well and falls apart on the details: accessibility gaps, missing native gestures, performance that's fine until it isn't.

The strong version is different. It's not one implementation stretched across platforms — it's one interface with a real implementation behind it on each platform, shipped and versioned together so they can't drift. The web build behaves like the web. The native build behaves like native. But the name, the props, and the result are identical, so from where you (and your agent) sit, it's one component. That second definition is the only one worth chasing, and it's the one this whole problem space is really about.

The one-API architecture — how it works in practice

Same component name, same props, same visual output on web and native

The core move is simple to state and unglamorous to enforce: every component exposes the same name, the same prop signature, and the same visual result on web and on mobile. A <Button variant="primary"> is a <Button variant="primary"> in both places. The agent doesn't learn two APIs. You don't maintain two mental models. There is one component that happens to have a web body and a native body underneath.

The one-API architecture in detail makes the case for why this is non-negotiable rather than nice-to-have: prop drift is the thing that kills cross-platform projects. The moment one side adds a prop the other doesn't have, or renames onClick to onPress without a unifying surface, you no longer have one component — you have two with a shared name, which is the worst of both worlds because it looks unified in the file tree and isn't. A merged interface is worth more than feature-drifted "parity." The contract is the product.

How the token layer bridges web and mobile design

Visual consistency across platforms isn't held together by copying hex values into two files. It's held together by a token layer: colors, spacing, type, radii, and motion defined once and compiled into the form each platform needs — CSS custom properties for the web build, plain style objects for the native build. The designer specifies the value once; both platforms consume it unchanged.

This is also why "it looks the same on both" survives a theme switch. When the surface, the accent, and the neutral ladder all derive from the same source, re-theming re-tints the whole component on both platforms at once. You're not maintaining a web palette and a mobile palette and praying they match — you're maintaining one, and the platforms read from it. The token layer is the quiet reason the visual half of the one-API promise holds.

When the implementation diverges — what stays shared, what doesn't

One API does not mean one identical implementation, and pretending otherwise is how you get the "write once, shoddily everywhere" result. Some things genuinely have to differ: a focus ring matters on the web and is meaningless on a touch screen; a haptic tap belongs on iOS and has no web equivalent; keyboard navigation is a desktop concern. The right pattern is to let the implementation be idiomatic to each platform while keeping the surface — the props you type, the states the component exposes — strictly shared and strictly typed.

So the rule is: the interface is shared, always; the internals diverge only where the platform forces it, and those divergences live behind the shared surface, not in front of it. Your code, and your agent, only ever see the one contract. The platform-specific shims stay where they belong — underneath.

The 2026 universal component landscape

What the newest Expo cycle changes for universal components

The mobile half of the universal stack got materially better in the most recent Expo cycles. Build times came down, the path from one codebase to iOS, Android, and web got smoother, and the rough edges that used to make "just add mobile" a multi-week project shrank. Universal components on the current Expo SDK covers what that makes practical — the same component tree producing real native apps and a real web build, without the universal-renderer compromises that used to come with the territory.

The honest read: the platform is no longer the bottleneck. A few years ago, "one codebase, three targets" came with enough caveats that most teams maintained separate apps anyway. That calculus has flipped — the remaining cost is in the design and context layer, not the build pipeline.

React Native in 2026 — what the new architecture enables

Where React Native sits in 2026 is the broader frame: the new architecture is no longer the experimental opt-in it was, and the performance and interop story is good enough that "native feel from a shared codebase" is a default expectation rather than a stretch goal. For the universal-component question specifically, that matters because it removes the old excuse — you used to accept that the shared version would feel a notch worse than a hand-built native app, and that's the gap that has mostly closed.

If you're tracking the moving parts, the ongoing React Native updates are worth following, because the handoff from a web prototype to a real native build depends on which version of the platform you're landing on.

Build speed: how precompiled native modules change the equation

One specific change is worth calling out because it changes the day-to-day, not just the spec sheet. Precompiled native modules cutting iOS build time is the kind of improvement that doesn't make headlines but quietly makes the whole approach viable for a small team. When a clean native build goes from a coffee-break wait to something closer to instant, the friction of "I'll just test this on a real device" drops, and the shared-codebase workflow stops feeling like a tax. Build speed is an adoption lever, not a footnote.

The agent-readability dimension

This is the part the rest of the field skips, and it's the part that decides whether any of the above actually ships.

Why a component without a registry entry is invisible to your agent

A component your agent can't discover may as well not exist. If there's no machine-readable registry entry — a structured record of the component's name, its props, its install path, its dependencies — then when your agent goes looking for "the date picker we use," it finds nothing and builds a new one. The registry entry is what turns a folder of components into a library an agent can use. Without it, every session starts from scratch and your codebase accretes near-duplicate implementations of things you already built.

Package architecture for agent-readable components gets into why this structure matters at the package level — the difference between a pile of files and a set of components an agent can enumerate, reason about, and reach for correctly. The registry is the index your agent reads before it writes.

The prompt library that ships with the component

The second piece of context is the prompts. A component that ships with a handful of tested prompts — "add a variant," "wire this to a form," "make an empty state for this list" — is a component your agent already knows how to extend, because the extension paths have been written down and tried. This is the difference between an agent that thrashes against an unfamiliar API and one that follows a known recipe. The prompts encode the intended way to grow the component, so the agent grows it that way instead of inventing a fifth pattern.

JSDoc as agent interface documentation

The third piece is the inline documentation. Structured JSDoc on a component's props isn't there for a hover tooltip you'll never read — it's the interface description your agent consumes when it decides what's safe to pass and what each prop does. A well-documented prop signature is a contract the agent can honor; an undocumented one is a guess. When the component, the registry entry, the prompts, and the JSDoc all describe the same one-API surface, the agent has everything it needs to extend the component correctly on both platforms in one pass — which is the whole point.

This is also where the cross-platform problem and the agent problem turn out to be the same problem. The reason your agent stalled at the web-to-native boundary at the top of this guide is that there was no coherent thing for it to read. Ship the component and the context that describes it — one contract, registered, documented, with worked examples — and the boundary disappears.

Getting to native mobile from a web prototype

The Lovable-to-native handoff and what it requires

A lot of cross-platform projects don't start cross-platform. They start as a web prototype in a tool like Lovable, the prototype works, and then someone asks for the phone version. The Lovable-to-native handoff is where the one-API question stops being theoretical. If the components in the prototype have no native twin, the handoff means rebuilding the UI for mobile from scratch — which is exactly the drift trap, just front-loaded. If they're built on a shared contract from the start, the handoff is mostly a matter of pointing the same components at a native target.

The deeper version of this is architectural, and it's worth understanding why the prototype tool itself can't make the jump — the difference between sandboxed and filesystem agents is the structural reason a tool that runs in an isolated container can't follow you into a real native build, no matter how good its web output is. The fix isn't a better sandbox; it's a codebase a filesystem agent can read and extend on your machine.

React Native updates and what changes in the handoff

The handoff isn't static — it depends on the platform version you land on, and the React Native updates worth tracking change what's involved from one cycle to the next. The practical advice is to do the handoff against a current, well-supported version rather than whatever your prototype tool happened to pin, because the gap between "old RN" and "current RN" is most of the friction in the move.

The announcement post behind the SDK

If you want the worked example rather than the argument, the SDK announcement post is the concrete version of everything above — what shipped, what the one-API surface looks like in real components, and how the web and native builds line up in practice. It's the proof that this isn't a thought experiment: same component, same props, same output, on web and on mobile, with the context an agent needs to extend it.

Building on the universal stack — when it's right and when it's not

The honest part of this guide: a shared cross-platform stack is the right call for most builders most of the time, but not all of the time. Here's how to decide.

Web-first projects that added mobile

If you started on the web and mobile is now a requirement, the universal stack is close to a no-brainer — it's the difference between rebuilding your UI for a phone and reusing it. The one-API contract is what makes "add mobile" a feature rather than a second project. This is the most common path, and the one where the payoff is largest, because you're avoiding a full second implementation.

Mobile-first projects that needed web

The reverse case — a mobile app that now needs a web presence (a dashboard, a settings page, a marketing surface that shares components) — benefits the same way, with one caveat: be honest about which screens genuinely need to be shared and which are web-only by nature. A shared component contract pays off for the screens that exist on both; it's overhead for the ones that don't. Use it where the overlap is real.

Decision criteria: native performance vs shared codebase

There's a real line where a shared codebase stops being the right answer: if your app's core value is a deeply platform-specific experience — heavy real-time graphics, frame-perfect gesture work, hardware integration with no web analog — the shared surface buys you less and the platform-specific work dominates anyway. For most CRUD-shaped products, dashboards, and content apps, that line is nowhere near where you are, and the shared stack wins. For the rare app at the edge of what the device can do, build native and don't fight it.

And one workflow note that ties this back to how you actually build: a universal component is only as useful as your agent's ability to find and extend it. Wiring the component registry into your agent's context — the registry entry, the JSDoc, the prompts, surfaced in the files your agent reads first — is what turns "we have a shared library" into "our agent reuses it instead of reinventing it." The stack and the context are two halves of the same decision.

The short version

The cross-platform UI question isn't a styling question anymore. The platform got good — the newest Expo and React Native cycles made "one codebase, three targets" a real default, not a compromise. What's left is the contract and the context: one component, with one prop signature and one visual result on web and on mobile, described in a way your agent can read, register, document, and extend without guessing. Ship only the first half and you get the orphaned-registry problem — components on both platforms, drifting apart, that no agent can keep in sync. Ship both halves and the web-to-native boundary that stalled your agent at the start of this guide simply goes away.

That second half — the same component, web and mobile, that your agent can actually read — is what the free MIT-licensed OTF SDK is. One API across both platforms, with the registry entries, JSDoc, and tested prompts that make a component extendable by the agent you already use, not just by you. It's the lead-magnet layer under the paid kits, and it's free to drop into whatever you're building. If you've hit the web-to-native wall, that's the part worth starting with — grab the SDK and point your agent at it.