expo-web-to-native
Migrate a web React app (Next.js, Vite, CRA) to a native iOS and Android app with Expo, mapping web idioms like the DOM, CSS, React Router and localStorage to native.
Install
npx skills add https://github.com/expo/skills --skill expo-web-to-nativeNative patterns: redesigning web UX for native
Disclosed reference for expo-web-to-native, step 4. false-friends.md translates idioms (div → View); this translates UX patterns - a web interaction into its native equivalent. Step 4 isn't a port, it's this redesign.
Reach for @expo/ui first. It renders real SwiftUI (iOS) and Jetpack Compose (Android), so its components look and feel exactly like the OS — the difference between "native-ish RN" and "indistinguishable from an app Apple/Google shipped." See the expo-ui skill. Drop to styled RN primitives only for what @expo/ui doesn't cover: custom layouts (chat bubbles, bespoke cards) and large data lists (@expo/ui List is a JS-thread node per item — use FlashList/FlatList for feeds). @expo/ui runs in Expo Go (SDK 56+) — no dev build needed; reach for a dev build (the expo-dev-client skill) only for custom native modules.
The test: if a screenshot of the native screen could pass for the web version, you reskinned it. Redesign until it could pass for a screen the OS shipped.
Feel — beyond the components
The table gets the right components; "native" also lives in motion and touch - and a web port arrives with almost none, so you must add it. The tooling is in expo-router (navigation/transitions) and expo-native-ui (motion/effects); reach for it, don't hand-roll:
- Transitions for free - use Expo Router's native-stack so push/pop, modals, and sheets animate with real platform physics; shared-element zoom via
expo-routerzoom-transitions.md. - Motion - Reanimated (
withSpring/withTiming) +react-native-gesture-handlerfor swipes/drags → load theexpo-animationskill (purpose gate, timing values, interruption). - Touch —
expo-hapticson commits, selection changes, and pull-to-refresh — choreography, not just a toast buzz. - Native rhythm — large-title collapse on scroll, momentum / inverted scroll, a keyboard that pushes content (
KeyboardAvoidingView). - Respect reduced motion — gate non-essential animation on Reanimated's
useReducedMotion().
Feel can't be screenshotted. A janky transition or wrong easing passes a still-image parity check and still betrays the app — for screens with motion, verify with a short recording (iOS xcrun simctl io booted recordVideo feel.mov; Android adb shell screenrecord; or a device-agent flow), not just a screenshot.