A phased approach to perfecting the mobile experience, from PWA fixes through Capacitor wrapping to an optional native Swift client.
Clarity is a Next.js 16 PWA deployed on Vercel with Turso/Drizzle, shadcn/ui, and Tailwind v4. It works well as a web app but has persistent iOS Safari tab bar issues.
iOS Safari's bottom toolbar height is not reflected in env(safe-area-inset-bottom). This causes the mobile navigation dock to either float too high (Safari browser) or get clipped (PWA standalone). The toolbar dynamically expands/collapses and no CSS value tracks it.
Fix the tab bar with the correct CSS pattern and add JavaScript-based Safari toolbar detection.
Never combine fixed height with safe-area padding on the same element. Separate them:
/* Outer container handles safe area */
.bottom-nav {
position: fixed;
bottom: 0;
width: 100%;
padding-bottom: env(safe-area-inset-bottom, 0px);
}
/* Inner container handles nav height */
.bottom-nav-content {
height: 56px;
display: flex;
align-items: center;
}
/* Body clearance */
body {
padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px));
}
The visualViewport API is the only reliable way to track Safari's dynamic toolbar:
// Detect standalone PWA vs Safari browser
const isStandalone =
window.matchMedia('(display-mode: standalone)').matches
|| window.navigator.standalone;
// In Safari browser, track the dynamic toolbar
if (!isStandalone && window.visualViewport) {
window.visualViewport.addEventListener('resize', () => {
const offset = window.innerHeight
- window.visualViewport.height;
document.documentElement.style
.setProperty('--safari-toolbar-h', `${offset}px`);
});
}
Required in <head> to enable safe area access:
<meta name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
Wrap the existing Next.js app with Capacitor for App Store distribution without rewriting the UI.
env(safe-area-inset-*) reports 0px — use capacitor-plugin-safe-areaA SwiftUI frontend talking to the same backend. Full native experience, zero WebView compromises.
Both frontends share the same API, auth (Better Auth), and data model. No duplication of business logic.
All options for getting the app on your phone (and your girlfriend's).
| Method | Cost | Expiry | Devices | Review? | Best For |
|---|---|---|---|---|---|
| Xcode Sideload | Free | 7 days | 3 apps max | None | Quick testing |
| Ad Hoc | $99/yr | 1 year | 100 devices | None | Personal use (recommended) |
| TestFlight | $99/yr | 90 days | 10,000 testers | Light | Beta testing |
| Unlisted App Store | $99/yr | None | Unlimited | Full | Private but discoverable by link |
| Public App Store | $99/yr | None | Unlimited | Full | Public launch |
If you publish to the App Store, here's when Apple would flag your app.
Which approach to use based on what you need.
| Need | PWA | Capacitor | Native Swift |
|---|---|---|---|
| Web + mobile from one codebase | Best | Good | Two codebases |
| Perfect iOS tab bars / gestures | Workaround | Better | Perfect |
| App Store presence | No | Yes | Yes |
| Push notifications | Web Push only | Native | Native |
| Development speed | Fastest | Fast | Slower |
| Maintenance burden | Lowest | Low-Med | High (2 UIs) |
| Offline / background sync | Service Worker | Better | Best |
| Widgets / Shortcuts | No | No | Yes |
Fix the PWA tab bar with visualViewport API + separated padding. This is the primary interface and should work flawlessly on iOS Safari and standalone mode.
Wrap with Capacitor. Add push notifications + haptics + one more native feature to pass App Store review. Use capacitor-plugin-safe-area for inset values. Distribute via Ad Hoc ($99/yr) for personal use or submit to the store.
Build a SwiftUI client against the same API. Full native experience. Only justified if the product has traction and WebView limitations frustrate users.