One component API for web, iOS, and Android unifies OTF's SDK design system
The hard thing about design systems isn’t the Figma file. It’s shipping the same component API, look, and feel to both web and mobile, so your codebase doesn’t fork the moment you want a button to work everywhere. Everyone claims "cross-platform," but look under the hood: props, theming, and even component names diverge. OTF’s approach is boringly strict — we force from @otfdashkit/ui (web) and from @otfdashkit/ui-native (mobile) to share not just a name, but a prop signature and visual tokens. The win: you swap a theme or change a prop, and your design intent lands the same way in browsers and on iOS/Android. The punchline: no more copy-pasting a look, no more chasing two sets of transient design-systems.
Let’s unpack how we built this, what it looks like in practice, and why fighting the cross-platform churn at the component and token level is the durability move.
One prop interface: the make-or-break constraint
Radix and Tamagui are as far apart as component libraries get. Radix lives for the DOM and React, Tamagui targets React Native primitives, ships its style objects to both native and web. Most shops treat this split as fatal. They maintain parallel Button implementations:
// web/Button.tsx
export function Button(props: { color?: string; onClick?: () => void }) {
// ...renders <button> or <div role="button">
}
// native/Button.tsx
export function Button(props: { color?: string; onPress?: () => void }) {
// ...renders <TouchableOpacity>
}This looks harmless until you realize every prop, event, and modifier is different. onClick vs. onPress, className vs. style, focus rings, active states, and accessibility all drift.
OTF cuts this off at the root. Every component in @otfdashkit/ui and @otfdashkit/ui-native shares the same name, same prop signature, and effect:
/* Shared API: works in both /ui and /ui-native */
<Button
size="lg"
variant="ghost"
icon={<Check />}
onPress={() => { /* navigate */ }}
/>The contract: any component used in the docs renders and behaves the same on all platforms, or it doesn’t ship.
Takeaway: Prop drift is the death of cross-platform. Merging interface is worth more than feature-drifted "parity."
Design tokens: one set, platform-native output
Most design systems punt on tokens: "We’ll handoff the Figma variables and restyle later." This always means two sets of tokens, two inconsistent palettes, two color modes, and hundreds of bugs.
@otfdashkit/tokens is a design-token compiler. You write one set of variables:
// tokens/otf.tokens.ts
export const tokens = {
color: {
primary: "#1e293b",
background: "#f8fafc",
border: "#64748b",
accent: "#818cf8",
},
radii: {
sm: 4,
md: 8,
lg: 16,
},
// ...typography, spacing
}On web, this compiles to CSS variables (using style-dictionary under the hood), shipped in a CSS file:
:root {
--color-primary: #1e293b;
--color-background: #f8fafc;
--color-border: #64748b;
--color-accent: #818cf8;
--radii-sm: 4px;
--radii-md: 8px;
--radii-lg: 16px;
/* ... */
}On native, tokens become JS objects (or themeable constants) consumed by Tamagui’s theme system:
// tokens/otf-theme.ts
export const otfTheme = {
colorPrimary: "#1e293b",
colorBackground: "#f8fafc",
colorBorder: "#64748b",
colorAccent: "#818cf8",
radiiSm: 4,
radiiMd: 8,
radiiLg: 16,
// ...
}Win: Switch a theme, and it flips perfectly on both web and native. No divergence in color, no guesswork mapping corner radii.
Visual identity enforced at the code level
The promise: is visually identical on every platform. This means:
- Same exact shape, spacing, radius.
- Same color palette, font, and icon scale.
- Same interaction affordances (active, disabled, focus state).
Enforcement is not hand-wavy. OTF ships a 24-item design checklist that runs before any component update merges. If the web and native screenshots don’t match, the code can’t ship.

Under the hood, this relies on:
- Radix Primitives for web (accessible, composable, CSS-in-JS).
- Tamagui primitives for native (React Native targets, style props).
- One design-token source, enforced build-to-build.
- Platform-specific shims only when unavoidable (e.g., focus ring on web, haptic feedback on iOS).
This is not an abstraction over platform. The surface is strictly typed, but the implementation is idiomatic to each.
A theme that flips everywhere, not just in the browser
Most "theming" demos are browser-only. Try switching dark mode on iOS or Android: colors are subtly off, buttons are too round or not round enough. You get theme drift.
@otfdashkit/tokens pushes a true "flip-one-theme" contract:
- On web: import the CSS var theme, toggle a class at the root, and instant dark mode.
- On native: Tamagui receives the theme object, and every styled prop—color, radius, font, shadow—resolves using the same tokens.
Concrete usage:
// Applying a theme (web)
import { ThemeProvider } from '@otfdashkit/ui'
import '@otfdashkit/tokens/otf-theme.css'
// ...in app root
<ThemeProvider theme="otf-dark">
<Button variant="accent" />
</ThemeProvider>
// Applying a theme (native)
import { ThemeProvider } from '@otfdashkit/ui-native'
import { otfTheme } from '@otfdashkit/tokens/otf-theme'
<ThemeProvider theme={otfTheme.dark}>
<Button variant="accent" />
</ThemeProvider>Flip the theme in one place. The UI stays visually locked. No more hunting cross-platform "almosts."
Why not React Native Web? (And why we tried)
The obvious question: "Why not write everything in React Native, export web with RNW, and go home?"
We tried. Here’s what broke:
- Accessibility: React Native Web never fully matches Radix’s web a11y surface.
- CSS features: Subtle browser features (focus rings, :has selectors, input autofill) are hard to polyfill cleanly.
- Workflow: Designers want to ship web-first landing pages with Next.js + Vite — not a monolithic Expo build.
- Package churn: Many web-only or mobile-only dependencies disagree, forcing manual patches and hacks.
- Native capacity: Fast native code (EAS, Expo Go) is not a drop-in replacement for browser perf.
In the end, using Radix + Tamagui with a strict API contract, and a real theme token layer, beats stretching a tool beyond its intent. We get real web, real native — with as much shared code as possible, not as little.
Shipping components, not half-parity
Every OTF component—Nav, Card, Notification, Sheet, Input—ships both as a web module (@otfdashkit/ui) and a native module (@otfdashkit/ui-native). The IDs, props, and look match. We don’t ship components that can’t meet the parity bar—no partial drops.
Installation matches the dev expectation:
# Web project
npm install @otfdashkit/ui @otfdashkit/tokens
# Native project (Expo)
npm install @otfdashkit/ui-native @otfdashkit/tokens tamaguiYou can import every component with the same name and props. If you swap from web to native or vice versa, you fix the import source, not the component surface:
// Web
import { Button } from '@otfdashkit/ui'
// Native
import { Button } from '@otfdashkit/ui-native'This is not a monorepo trick. Both targets get first-class support, and you own every shipped file.
Fast iteration: copy-paste or npm, your call
Starter kits and design systems obsess over philosophy. Our stance: you should be able to npm install for velocity, or copy-paste for lock-in:
npx otf-initdrops in the entire folder tree, just like shadcn, so you have source-of-truth in-repo.- Or, you
import { Button } from '@otfdashkit/ui'and treat it as a dependency—no forking required, full theming still works.
This matches how most teams actually ship. You get velocity or total control.
What this enables: durable UI, faster launches
With one contract for tokens and props, you get:
- Sketch-to-shipping runs in one pass: designers specify the look, you flip tokens and it works everywhere.
- No design-dev drift: Figma tokens are source-of-truth, code consumes them unchanged.
- Theme changes land everywhere—in one commit, not two barely-consistent ones.
- You ship real native apps (Expo, EAS build) and production-grade web (Vite or Next.js) from the same kit.

Contrast: Traditional two-stack shops end up rewriting UI for every marketing push, every new color mode, every accessibility review. OTF removes that dead work.
Closing: design shouldn’t get rebuilt for every platform
If you’re tired of living in two worlds—one for web, one for mobile—but don’t buy "write once, shoddily everywhere," try the OTF method: shared tokens, strict prop contracts, local themes, zero second-guessing what your button looks like. Engineers get velocity and a codebase that endures. Designers see their work land as intended. The rest is churn you never need to debug again.
Ship with a single source of visual truth—across every screen.