Next.js + Capacitor Setup
Configure a Next.js project to work with Capacitor for mobile app development.
What you'll accomplish
Have a Next.js project configured and building for Capacitor.
Prerequisites
- Node.js 18+ installed
- Basic Next.js knowledge
- Completed "What is Capacitor?" lesson
Overview
To use Next.js with Capacitor, we need to:
- Configure Next.js for static export
- Install and initialize Capacitor
- Set up the project structure
- Build and sync to native platforms
Note
Static Export Recommended: For apps distributed through the App Store or Google Play, bundling static HTML files is the standard approach. Capacitor can load remote URLs (useful for development live-reload or apps that always require a network connection), but static assets are recommended for store-bound binaries because they work offline and provide a faster initial load.
Step 1: Create or Configure Next.js Project
New Project
bashnpx create-next-app@latest my-mobile-app cd my-mobile-app
Choose these options:
- TypeScript: Yes (recommended)
- ESLint: Yes
- Tailwind CSS: Yes (optional but popular)
- App Router: Yes
- Import alias: Yes (@/*)
Existing Project
If you have an existing project, continue to the next step.
Step 2: Configure Static Export
For store-distributed apps, static HTML files are recommended. Configure Next.js for static export:
typescript// next.config.ts import type { NextConfig } from 'next'; const nextConfig: NextConfig = { output: 'export', // Disable image optimization (not available in static export) images: { unoptimized: true, }, // Optional: Set base path if deploying to subdirectory // basePath: '/app', // Optional: Trailing slashes for cleaner URLs trailingSlash: true, }; export default nextConfig;
Test Static Export
Build and verify:
bashnpm run build
Check the out folder - it should contain your static HTML files.
Static export doesn't support:
- Server Actions
- API Routes (use external APIs instead)
- Dynamic routes without
generateStaticParams revalidatein fetch (use client-side fetching)- Middleware
- Headers/redirects in next.config.js
Workarounds:
- Move server logic to external APIs
- Use client-side data fetching
- Pre-generate dynamic routes at build time
Alternative: Remote URL Loading
If your app requires server-side features, you can configure Capacitor to load from a remote URL instead of bundled assets. This means your app acts as a wrapper around a web app hosted elsewhere. Trade-offs include:
- Requires network connectivity
- App Store/Play Store reviewers may scrutinize apps that are primarily web wrappers
- Faster iteration (no rebuild/resubmit for content changes)
See the Capacitor documentation on loading external URLs for details.
Step 3: Install Capacitor
Install the core Capacitor packages:
bashnpm install @capacitor/core @capacitor/cli
Initialize Capacitor:
bashnpx cap init
You'll be asked:
- App name: Your app's display name (e.g., "My Mobile App")
- App ID: Reverse domain format (e.g.,
com.yourcompany.mymobileapp)
The App ID is a unique identifier for your app, written in reverse domain format:
com.yourcompany.appnameio.github.yourusername.appname
This ID:
- Must be unique worldwide
- Can't be changed after publishing
- Is used by app stores to identify your app
Use a domain you own to ensure uniqueness.
Step 4: Configure Capacitor
Update capacitor.config.ts:
typescript// capacitor.config.ts import type { CapacitorConfig } from '@capacitor/cli'; const config: CapacitorConfig = { appId: 'com.yourcompany.mymobileapp', appName: 'My Mobile App', webDir: 'out', // Next.js static export directory // Optional: Development server for live reload server: { url: 'http://localhost:3000', cleartext: true, // Allow HTTP (only for development!) }, // iOS-specific settings ios: { contentInset: 'automatic', }, // Android-specific settings android: { allowMixedContent: true, // Only for development }, }; export default config;
Warning
The server.url setting is for development only. Remove it (or comment it out) before production builds!
Step 5: Add Native Platforms
Add iOS
bashnpm install @capacitor/ios npx cap add ios
This creates an ios folder with an Xcode project.
Add Android
bashnpm install @capacitor/android npx cap add android
This creates an android folder with an Android Studio project.
Step 6: Build and Sync
The workflow is:
- Build your Next.js app:
npm run build - Sync to native platforms:
npx cap sync
Create a helper script:
json// package.json { "scripts": { "build": "next build", "cap:sync": "npx cap sync", "cap:build": "npm run build && npm run cap:sync", "cap:ios": "npm run cap:build && npx cap open ios", "cap:android": "npm run cap:build && npx cap open android" } }
Now:
npm run cap:ios- Build and open in Xcodenpm run cap:android- Build and open in Android Studio
Step 7: Development Workflow
Option 1: Live Reload (Recommended for Development)
Keep your Next.js dev server running:
bashnpm run dev
In another terminal, start the native app pointing to your dev server:
bashnpx cap run ios # iOS Simulator npx cap run android # Android Emulator
Changes to your web code appear instantly!
Option 2: Static Builds
For testing production behavior:
bashnpm run cap:build npx cap run ios
This uses the built static files.
Project Structure
After setup, your project looks like:
my-mobile-app/
├── src/
│ └── app/ # Next.js app
├── ios/ # iOS Xcode project
│ └── App/
├── android/ # Android Studio project
│ └── app/
├── out/ # Static export (after build)
├── capacitor.config.ts
├── next.config.ts
└── package.json
Handling Common Issues
Images Not Loading
With static export, use unoptimized images:
tsx// Before (won't work) import Image from 'next/image'; <Image src="/photo.jpg" width={100} height={100} /> // After (works) <img src="/photo.jpg" width={100} height={100} alt="Photo" /> // Or configure in next.config.ts images: { unoptimized: true }
API Routes Not Working
Static export doesn't support API routes. Options:
Option 1: External API
Host your API separately and call it:
typescript// Instead of /api/users, call: const response = await fetch('https://api.yoursite.com/users');
Option 2: Serverless Functions
Use Vercel Functions, AWS Lambda, etc. separately from your Capacitor app.
Option 3: Client-Side Only
For simple apps, handle everything client-side:
typescript// Local storage instead of database localStorage.setItem('user', JSON.stringify(user));
Dynamic Routes
For dynamic routes, use generateStaticParams:
typescript// src/app/posts/[id]/page.tsx export async function generateStaticParams() { const posts = await fetchPosts(); return posts.map((post) => ({ id: post.id.toString(), })); } export default function PostPage({ params }: { params: { id: string } }) { // ... }
Safe Area Handling
Mobile devices have notches and home indicators. Handle them:
css/* globals.css */ :root { --safe-area-inset-top: env(safe-area-inset-top); --safe-area-inset-bottom: env(safe-area-inset-bottom); --safe-area-inset-left: env(safe-area-inset-left); --safe-area-inset-right: env(safe-area-inset-right); } /* Apply to layout */ .app-container { padding-top: var(--safe-area-inset-top); padding-bottom: var(--safe-area-inset-bottom); padding-left: var(--safe-area-inset-left); padding-right: var(--safe-area-inset-right); }
Add the viewport meta tag:
tsx// src/app/layout.tsx export const metadata: Metadata = { viewport: 'width=device-width, initial-scale=1, viewport-fit=cover', };
Environment Variables
For Capacitor apps, use build-time environment variables:
bash# .env.local NEXT_PUBLIC_API_URL=https://api.example.com
Access in code (must use NEXT_PUBLIC_ prefix):
typescriptconst apiUrl = process.env.NEXT_PUBLIC_API_URL;
Warning
Security note: NEXT_PUBLIC_ variables are embedded in your JavaScript and visible to users. Never put secrets here!
Common Issues
Frequently Asked Questions
All dynamic routes need generateStaticParams to tell Next.js which paths to pre-render.
typescriptexport async function generateStaticParams() { return [{ id: '1' }, { id: '2' }]; }
Check:
- Did you run
npm run buildbeforecap sync? - Is
webDir: 'out'correct in capacitor.config.ts? - Are there console errors in Safari/Chrome dev tools?
Run npm install first. Then try again.
For static builds: Run npm run cap:build again.
For live reload: Make sure dev server is running at the URL in capacitor.config.ts.
Client-side only. Server Components that fetch data need to be converted to client components with useEffect or SWR/React Query for data fetching.
Verify Your Understanding
You've completed this lesson if you can:
Next: Set up iOS development with Xcode.