Capacitor
30 minutes
linux
macos
windows

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:

  1. Configure Next.js for static export
  2. Install and initialize Capacitor
  3. Set up the project structure
  4. 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

bash
npx 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:

bash
npm 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
  • revalidate in 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:

bash
npm install @capacitor/core @capacitor/cli

Initialize Capacitor:

bash
npx 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.appname
  • io.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

bash
npm install @capacitor/ios
npx cap add ios

This creates an ios folder with an Xcode project.

Add Android

bash
npm 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:

  1. Build your Next.js app: npm run build
  2. 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 Xcode
  • npm 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:

bash
npm run dev

In another terminal, start the native app pointing to your dev server:

bash
npx 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:

bash
npm 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):

typescript
const 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.

typescript
export async function generateStaticParams() {
  return [{ id: '1' }, { id: '2' }];
}

Check:

  1. Did you run npm run build before cap sync?
  2. Is webDir: 'out' correct in capacitor.config.ts?
  3. 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:

0/5

Next: Set up iOS development with Xcode.