Write <Button> once and ship it consistently on web, iOS, and Android
Writing <Button> once and shipping it on web, iOS, and Android with the same props and the same name sounds simple until you spend weeks fixing divergent details across stacks. The promise is real: ship fast, swap themes in one place, wire analytics once, and never ask which Button is which again.
The thesis here is structural, not cosmetic. One component API across platforms lets small teams deliver features at a pace that used to require a full mobile squad.
React Native gives the underlying primitive — one JavaScript and TypeScript codebase rendering truly native iOS and Android UI rather than a web view (React Native — getting started). Expo extends that with a single project that runs natively across phones plus web-friendly tooling for routing, builds, and TestFlight distribution (Expo docs).
Duplicated UI is a tax you always pay
Build web and mobile the classic way and you keep two repos: a web app in React or similar, and a mobile app in Swift, Kotlin, React Native, or Flutter. Every button, card, and input becomes a negotiation.
The web Button supports asChild and forwards refs. The mobile variant takes children or a label but never both. Spacing and color drift. State props like loading or disabled need separate visual handling per target. A simple request like making the Button a pill turns into an afternoon of small PRs in both places.
AI codegen makes the drift worse when APIs differ. An agent adds the feature on one side and guesses on the other, and web drift versus mobile drift compounds. The easy answer of just componentizing falls apart because the components do not share a contract.
Naming, props, and structure must line up
Web and mobile disagree well below the pixel level. Names, prop signatures, and valid composition patterns drift:
// Web (React)
<Button asChild disabled>Save</Button>
// Mobile (React Native style)
<Button disabled label="Save" />Those differences partly reflect real platform idioms, which is why the claim should be consistency of API and intent — not pixel-perfect identical rendering on every OS. Platform type, safe areas, and accessibility conventions still differ, and honest cross-platform systems hedge that.
But when prop names and composition rules line up, static analysis, codemods, and AI agents can target one pattern. When they do not, every refactor risks silent breakage and teams simply avoid refactors. That avoidance is where apps grow cobwebs.
Pair this contract thinking with our same component for web and mobile guide and the broader one codebase, three platforms playbook.
One codebase. iOS, Android, and web.
The Fitness Kit ships with auth, a database, and a backend already connected — no setup. Live demo at fitness-preview.otf-kit.dev.
What write once with one API actually means
The false promise is reuse logic but rebuild every view. A shared component API means something stricter:
- Same import path, for example
import { Button } from "@otfdashkit/ui". - Same interface with every prop present and type-checked on each target.
- Theme and size handled by shared tokens, not forked media queries.
- Internals can swap per target at build time, but the public API does not leak those differences.
// One import, one API across web, iOS, and Android.
<Button
iconStart={<UserIcon />}
loading={isSaving}
disabled={!formValid}
onPress={handleSave}
>
Save
</Button>Feature work, theming, or analytics injection then touches one code path. Updating all Buttons actually updates all Buttons.
React Native is explicit that it targets real native widgets from one language, which is what makes this shared-API story plausible in the first place (React Native — getting started).
One token layer for every surface
Styling drift usually comes from spacing, color, and type being tuned or hardcoded per platform. A single token source fixes that:
// Unified tokens used by web and native builds.
export const tokens = {
color: { primary: "#3264fe", accent: "#e65100" },
radius: { button: 8, input: 6 },
size: { sm: 32, md: 44, lg: 56 },
};Flip brand color, size scale, or corner radius once and every component on every screen follows. Dark mode or a seasonal rebrand ships everywhere together. No Figma reconciliation pass to check whether mobile fell behind.
This is the same design-system-as-contract idea we cover in design systems as agent context: tokens are the file both humans and agents can trust.
One source of interaction truth
Native and web cue users differently — tap highlights, focus rings, accessibility hints, loading timing. Keeping two logic sources breaks parity in subtle ways.
<Button loading={isLoading} onPress={handleAction}>
{isLoading ? "Processing…" : "Submit"}
</Button>Animation, accessibility states, and interaction timing should follow one model everywhere, with platform adapters handling only what the OS genuinely requires. That keeps behavior predictable for users and for automated tests.
What it feels like in a real project
Solo builders cannot babysit three targets. With a single component API, work that used to mean changing web, then remembering what breaks on iOS, collapses to one path:
npm install @otfdashkit/ui @otfdashkit/ui-native @otfdashkit/tokensImport anywhere:
import { Button, Card, Input } from "@otfdashkit/ui";Use the same components in a Next.js or Vite web app and in a React Native and Expo native build. Props match, signatures match, and visual tweaks flow from tokens. Expo's single-project native workflow with routing, EAS builds, and TestFlight support is what carries the mobile half (Expo docs).
Internals can still use best-in-class primitives per target. Upgrading one side should not shift your public API or force a compatibility PR.
Where the savings compound
Three places to fix means every refactor costs three times the work and three times the testing surface:
| Task | Separate stacks | Single shared API |
|---|---|---|
| Add loading state | Two to three updates | One update |
| Theme change | Two to three themes | One token set |
| Analytics or haptic hooks | Multiple code paths | One hook |
| Accessibility support | Multiple checklists | One enforced checklist |
| Agent extension | Per-target configs | One stable contract |
Big teams absorb dual-stack cost with headcount. Small teams cannot, which is why the shared API matters more the leaner you are.
Why agents need the same contract
Agent-assisted development only works when the agent can recognize, use, and extend existing code. When every Button, Card, Sheet, and Modal is API-predictable and prop-stable, codegen tools extend instead of regenerating whole screens and losing state.
Every OTF kit ships with agent-readable configs such as CLAUDE.md and .cursorrules plus tested prompts, so the agent extends the kit rather than replacing it. Stable interfaces plus token-driven visuals are what make that automation reliable.
Owning a single, stable, token-driven UI set keeps output shippable while tools, native APIs, and form factors keep shifting. Start from production-ready foundations at OTF templates, theme from one codebase, and ship without the platform tax.
Sources
- React Native — getting started — cross-platform basis: one JavaScript and TypeScript codebase targeting native iOS and Android UI.
- Expo docs — single-project native workflow, Expo Router, EAS builds, and TestFlight distribution path.
Stop wiring. Start shipping.
- Login, database, and backend already connected — nothing to set up
- iOS + Android + web from one codebase
- AI configs pre-tuned + 40+ tested prompts included