The migration that breaks halfway through
You build a web app. It works well. You decide to ship it as a mobile app using a framework that wraps web apps in a native shell. You run the converter and hit a wall:
window.print()calls that crash on mobileonMouseEnterhandlers that have no touch equivalent- Fixed-width layouts that overflow at 375px
- CSS hover states with no fallback for touch
navigator.clipboardcalls that require browser permissions your shell doesn't grant
None of these are edge cases. They're standard web APIs that don't translate to native. If they're spread through your codebase, the migration is a rewrite, not a wrap.
The constraint list that prevents this
If native migration is a possibility, treat these as hard constraints from the first line of code:
Touch targets: every interactive element must be at least 44×44px. This is Apple's HIG requirement and Android's material spec. A 24px icon button that works fine with a cursor is unfixable with CSS alone on mobile.
No web-only events: onMouseEnter, onMouseLeave, onMouseOver have no touch equivalent. Use onClick and manage state explicitly. A dropdown that opens on hover needs a tap-to-open version on mobile anyway — build that version first.
No web-only APIs: window.print(), document.execCommand(), browser-specific clipboard APIs, and anything under window.location that assumes browser navigation. Replace with platform-agnostic equivalents before they proliferate.
Relative URLs everywhere: never hardcode http://localhost:3000 or a production URL in component code. Use relative paths (/api/...) or a config constant. Native shells proxy API calls differently.
375px as the design baseline: test at 375px (iPhone SE viewport) before considering any layout done. Nothing should overflow horizontally. Scroll should be intentional, not accidental.
Mobile-first CSS in practice
Write your base styles for small screens. Scale up with sm:, md:, lg: breakpoints:
/* Base: mobile */
.grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
/* Tablet and above */
@media (min-width: 640px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
This is the opposite of how many desktop-first apps are built (wide layout first, media queries shrink it down). Mobile-first means the mobile layout is the default, and you progressively enhance upward.
What the native shell can and can't do
Native shell frameworks (Capacitor, Cordova, React Native WebView) give your web app:
- Access to native device APIs (camera, push notifications, biometrics)
- App store distribution
- Offline capability with service workers
They do not fix:
- Web APIs that have no native equivalent
- Layout that doesn't work at mobile viewports
- Performance issues from heavy JavaScript rendering
The shell is a packaging tool, not a compatibility layer. Your web app needs to already work on mobile before the shell adds value.
The real cost of retrofitting
Mobile-first constraints feel like overhead when you're building for desktop first. The actual overhead is minimal — it's mostly about discipline in which APIs you use and which breakpoint you design from first.
The cost of retrofitting is much higher. Going through an existing codebase to replace mouse events, fix viewport overflows, and remove web-only APIs is tedious, error-prone work. Building it correctly once costs less.