Mobile frontend optimization for Vite + Capacitor apps targeting iOS and Android App Store. This skill should be used when optimizing mobile layouts, fixing safe area issues, improving touch targets, handling keyboard avoidance, optimizing Capacitor WebView performance, or preparing a Vite app for App Store submission. Triggers include "mobile layout", "capacitor", "safe area", "iOS", "Android", "touch target", "app store", "splash screen", "status bar", "keyboard", "WebView".
Provide production-ready patterns for building and optimizing React + Vite apps running inside Capacitor WebViews for iOS and Android App Store distribution. Covers safe areas, touch targets, performance, and store submission requirements.
Stack: React · Vite · Capacitor · Tailwind CSS · Shadcn UI
Apply this skill when the task involves:
Set viewport-fit=cover in :
index.html<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
Use CSS environment variables for insets:
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
Prefer 100dvh over 100vh — 100vh ignores the browser chrome on mobile:
min-height: 100dvh; /* dynamic viewport height */
Define CSS custom properties once on :root and reuse everywhere (see references/css-mobile-patterns.md).
min-h-[44px] min-w-[44px]p-3 (12px) padding on icon buttons to expand hit area without changing visual sizetouch-manipulation to interactive elements to eliminate 300ms tap delay:
touch-action: touch-manipulation;
min-h-[44px] min-w-[44px] flex items-center justify-centerfont-size: 16px on all <input>, <textarea>, <select> — iOS zooms in on anything smallerfont-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
user-select: none to non-text UI elements (buttons, nav, icons) to prevent accidental text selection on long-presstext-size-adjust: none — it breaks accessibilityUse @capacitor/status-bar to control color and style:
import { StatusBar, Style } from '@capacitor/status-bar';
await StatusBar.setStyle({ style: Style.Dark });
await StatusBar.setBackgroundColor({ color: '#ffffff' });
Call on app resume and after navigation to dark/light screens.
Use @capacitor/keyboard with resizeOnFullScreen: true in capacitor.config.ts. Listen to keyboard events to adjust scroll position:
import { Keyboard } from '@capacitor/keyboard';
Keyboard.addListener('keyboardWillShow', ({ keyboardHeight }) => {
// shift focused input into view
});
Never rely on CSS position: fixed for inputs — it breaks on iOS when keyboard opens.
Register a listener to prevent accidental app exit and handle modal/sheet dismissal:
import { App } from '@capacitor/app';
App.addListener('backButton', ({ canGoBack }) => {
if (!canGoBack) App.exitApp();
else window.history.back();
});
See docs/ANDROID_BACK_BUTTON.md for the full pattern used in this project.
Configure @capacitor/splash-screen in capacitor.config.ts. Hide programmatically after app is ready:
import { SplashScreen } from '@capacitor/splash-screen';
await SplashScreen.hide({ fadeOutDuration: 300 });
Never auto-hide — wait for data/auth to load first to avoid flash of unauthenticated content.
AndroidManifest.xmlposition: fixed elements can flicker during scroll — use transform: translateZ(0) to promote to GPU layerUse transform instead of top/left/margin for animations — avoids layout reflow:
/* ✅ GPU-composited */