Introduction
Want to build a mobile app with Nuxt from the ground up? This guide walks you through creating a brand new Nuxt 4 project configured for mobile from day one, then packaging it as native iOS and Android apps using Capacitor 8.
By the end of this tutorial, you’ll have a working mobile app running on simulators that you can continue developing and eventually publish to the App Store and Google Play.
Time required: ~30 minutes
What you’ll build:
- A new Nuxt 4 project with the latest directory structure
- Static generation configuration for mobile
- Capacitor 8 with essential plugins
- Native iOS and Android apps
- Live reload development setup
Already have a Nuxt app? Check out Convert Your Nuxt App to Mobile instead.
Prerequisites
Make sure you have these installed:
- Node.js 18+ (check with
node --version) - Bun package manager (
curl -fsSL https://bun.sh/install | bash) - Xcode (macOS only, for iOS development)
- Android Studio (for Android development)
Step 1: Create a New Nuxt 4 Project
Start by creating a fresh Nuxt 4 project:
bunx nuxi@latest init my-mobile-appcd my-mobile-appbun installNuxt 4 Directory Structure
Nuxt 4 uses a new directory structure with app code in the app/ directory:
my-mobile-app/ app/ assets/ components/ composables/ layouts/ middleware/ pages/ plugins/ utils/ app.vue public/ server/ nuxt.config.ts package.jsonThis structure provides better separation between app and server code.
Step 2: Configure Nuxt for Static Generation
Capacitor requires static HTML/JS/CSS files. Configure Nuxt for static generation in nuxt.config.ts:
export default defineNuxtConfig({ compatibilityDate: '2025-01-15', devtools: { enabled: true },
// Enable static generation ssr: true, nitro: { preset: 'static', },});Step 3: Add Mobile Scripts
Update your package.json with mobile development scripts:
{ "scripts": { "dev": "nuxt dev", "build": "nuxt build", "generate": "nuxt generate", "preview": "nuxt preview", "mobile": "bun run generate && bunx cap sync", "mobile:ios": "bun run mobile && bunx cap open ios", "mobile:android": "bun run mobile && bunx cap open android" }}Test the static generation:
bun run generateYou should see a .output/public directory with your static files.
Step 4: Install Capacitor 8
Install the Capacitor core packages:
bun add @capacitor/corebun add -D @capacitor/cliInstall essential plugins that most mobile apps need:
bun add @capacitor/app @capacitor/keyboard @capacitor/splash-screen @capacitor/status-bar @capacitor/preferencesWhat these plugins do:
- @capacitor/app — App lifecycle events (foreground/background, deep links)
- @capacitor/keyboard — Control keyboard behavior
- @capacitor/splash-screen — Native splash screen control
- @capacitor/status-bar — Style the device status bar
- @capacitor/preferences — Key-value storage (like localStorage but native)
Step 5: Initialize Capacitor
Initialize Capacitor with your project details:
bunx cap init "My Mobile App" com.example.mymobileapp --web-dir .output/publicReplace:
"My Mobile App"with your app’s display namecom.example.mymobileappwith your app ID (reverse domain notation)
This creates capacitor.config.ts. Update it with plugin configuration:
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = { appId: 'com.example.mymobileapp', appName: 'My Mobile App', webDir: '.output/public', plugins: { SplashScreen: { launchShowDuration: 2000, launchAutoHide: true, androidScaleType: 'CENTER_CROP', splashFullScreen: true, splashImmersive: true, }, Keyboard: { resize: 'body', resizeOnFullScreen: true, }, StatusBar: { style: 'dark', }, },};
export default config;Step 6: Add Native Platforms
Install the platform packages:
bun add @capacitor/ios @capacitor/androidGenerate the native projects:
bunx cap add iosbunx cap add androidThis creates ios and android directories containing the native projects.
Step 7: Build and Run
Build your project and sync with native platforms:
bun run mobileOpen in iOS Simulator:
bun run mobile:iosOr Android Emulator:
bun run mobile:androidIn Xcode (iOS):
- Select a simulator from the device dropdown
- Click the Play button or press
Cmd + R
In Android Studio:
- Wait for Gradle to finish syncing
- Select an emulator from the device dropdown
- Click the Run button or press
Shift + F10
Step 8: Set Up Live Reload
For faster development, enable live reload so changes appear instantly on your device.
- Find your local IP address:
# macOSipconfig getifaddr en0
# Windowsipconfig- Create a development Capacitor config. Update
capacitor.config.ts:
import type { CapacitorConfig } from '@capacitor/cli';
const devConfig: CapacitorConfig = { appId: 'com.example.mymobileapp', appName: 'My Mobile App', webDir: '.output/public', server: { url: 'http://YOUR_IP_ADDRESS:3000', cleartext: true, }, plugins: { // ... same plugin config },};
const prodConfig: CapacitorConfig = { appId: 'com.example.mymobileapp', appName: 'My Mobile App', webDir: '.output/public', plugins: { // ... same plugin config },};
const config = process.env.NODE_ENV === 'development' ? devConfig : prodConfig;
export default config;- Start the dev server and copy config to native:
bun run dev &NODE_ENV=development bunx cap copy- Rebuild in Xcode/Android Studio
Now edits to your Nuxt code will hot-reload on the device.
Step 9: Create Your First Mobile Screen
Let’s create a mobile-friendly home screen. Update app/app.vue:
<template> <NuxtPage /></template>Create app/pages/index.vue:
<template> <main class="min-h-screen bg-gradient-to-b from-green-500 to-green-700 flex flex-col items-center justify-center p-6 text-white" > <h1 class="text-4xl font-bold mb-4">My Mobile App</h1> <p class="text-xl mb-8 text-center opacity-90"> Built with Nuxt 4 + Capacitor 8 </p>
<div v-if="appInfo" class="bg-white/20 rounded-lg p-4 backdrop-blur-sm mb-8"> <p class="text-sm"> {{ appInfo.name }} v{{ appInfo.version }} </p> </div>
<div class="space-y-4 w-full max-w-sm"> <button class="w-full py-4 px-6 bg-white text-green-600 rounded-xl font-semibold text-lg shadow-lg active:scale-95 transition-transform" @click="handleGetStarted" > Get Started </button> <button class="w-full py-4 px-6 bg-white/20 text-white rounded-xl font-semibold text-lg backdrop-blur-sm active:scale-95 transition-transform" @click="handleShare" > Share App </button> </div> </main></template>
<script setup lang="ts">import { ref, onMounted, onUnmounted } from 'vue';import { App } from '@capacitor/app';
const appInfo = ref<{ name: string; version: string } | null>(null);
let backButtonListener: { remove: () => void } | null = null;
onMounted(async () => { // Get app info try { appInfo.value = await App.getInfo(); } catch (e) { // Web fallback appInfo.value = { name: 'My Mobile App', version: '1.0.0' }; }
// Handle Android back button backButtonListener = await App.addListener('backButton', ({ canGoBack }) => { if (!canGoBack) { App.exitApp(); } else { window.history.back(); } });});
onUnmounted(() => { backButtonListener?.remove();});
function handleGetStarted() { // Navigate to onboarding or main app console.log('Get started clicked');}
async function handleShare() { // We'll implement this with the Share plugin later console.log('Share clicked');}</script>Step 10: Add Tailwind CSS
For the styling to work, add Tailwind CSS to your project:
bun add tailwindcss @tailwindcss/viteUpdate nuxt.config.ts:
import tailwindcss from '@tailwindcss/vite';
export default defineNuxtConfig({ compatibilityDate: '2025-01-15', devtools: { enabled: true },
ssr: true, nitro: { preset: 'static', },
css: ['~/assets/css/main.css'],
vite: { plugins: [tailwindcss()], },});Create app/assets/css/main.css:
@import 'tailwindcss';
:root { --sat: env(safe-area-inset-top); --sar: env(safe-area-inset-right); --sab: env(safe-area-inset-bottom); --sal: env(safe-area-inset-left);}
body { padding-top: var(--sat); padding-right: var(--sar); padding-bottom: var(--sab); padding-left: var(--sal);}
/* Prevent text selection on mobile */* { -webkit-user-select: none; user-select: none; -webkit-tap-highlight-color: transparent;}
/* Allow text selection in inputs */input,textarea { -webkit-user-select: auto; user-select: auto;}Step 11: Add the Share Plugin
Let’s implement the share button functionality:
bun add @capacitor/shareUpdate app/pages/index.vue to use the Share plugin:
<script setup lang="ts">import { ref, onMounted, onUnmounted } from 'vue';import { App } from '@capacitor/app';import { Share } from '@capacitor/share';
// ... existing code ...
async function handleShare() { try { await Share.share({ title: 'Check out this app!', text: 'Built with Nuxt 4 and Capacitor 8', url: 'https://capacitorjs.com', dialogTitle: 'Share with friends', }); } catch (e) { console.log('Share cancelled or failed:', e); }}</script>Sync and rebuild:
bun run mobileProject Structure
Your project should now look like this:
my-mobile-app/├── android/ # Android native project├── ios/ # iOS native project├── .output/│ └── public/ # Static build output├── app/│ ├── assets/│ │ └── css/│ │ └── main.css│ ├── pages/│ │ └── index.vue│ └── app.vue├── capacitor.config.ts # Capacitor configuration├── nuxt.config.ts # Nuxt configuration├── package.json└── ...Next Steps
You now have a working Nuxt mobile app. Here’s what to do next:
Essential Setup
- App Icons: Replace default icons in
ios/App/App/Assets.xcassetsandandroid/app/src/main/res - Splash Screen: Customize in native projects or use
@capacitor/splash-screenconfig - Deep Links: Configure URL schemes for your app
Add More Features
- Camera:
bun add @capacitor/camera - Geolocation:
bun add @capacitor/geolocation - Push Notifications:
bun add @capacitor/push-notifications - File System:
bun add @capacitor/filesystem
UI Enhancement
Consider adding Konsta UI for native-looking iOS/Android components:
bun add konstaThen update your CSS to import the theme:
@import 'tailwindcss';@import 'konsta/theme.css';Over-the-Air Updates
Set up Capgo to push updates without app store resubmission:
bunx @capgo/cli initTroubleshooting
Build fails with “Cannot find module”
Run bun install and try again.
iOS: “No signing identity found” Open Xcode, go to Signing & Capabilities, and select your development team.
Android: “SDK location not found”
Create android/local.properties with sdk.dir=/path/to/android/sdk
Changes not appearing on device
Make sure you ran bun run mobile after making changes. For live reload, verify the IP address is correct and the dev server is running.
.output/public is empty or missing
Make sure you configured nitro: { preset: 'static' } in nuxt.config.ts and run bun run generate.
Resources
- Capacitor 8 Documentation
- Nuxt 4 Documentation
- Capgo - Live Updates
- Konsta UI - Mobile UI Components
Ready to ship your app? Learn how Capgo can help you deliver updates faster — sign up for a free account today.