Saltar al contenido principal
Tutorial

Crea una app móvil con Nuxt desde cero con Capacitor 8

Guía paso a paso para crear un nuevo proyecto Nuxt 4 y convertirlo en apps nativas para iOS y Android usando Capacitor 8. Ideal para empezar desde cero con desarrollo Vue mobile-first.

Crea una app móvil con Nuxt desde cero con Capacitor 8 Capgo

Introducción

¿Quieres crear una app móvil con Nuxt desde cero? Esta guía te explica cómo crear un proyecto Nuxt 4 configurado para móvil desde el primer día y luego empaquetarlo como apps nativas de iOS y Android usando Capacitor 8.

Al terminar este tutorial, tendrás una app móvil funcionando en simuladores que podrás seguir desarrollando y, finalmente, publicar en la App Store y Google Play.

Tiempo necesario: ~30 minutos

Qué vas a construir:

  • Un nuevo proyecto Nuxt 4 con la última estructura de directorios
  • Configuración de generación estática para móvil
  • Capacitor 8 con plugins esenciales
  • Apps nativas para iOS y Android
  • Entorno de desarrollo con Live reload

¿Ya tienes una app de Nuxt? Echa un vistazo: Convierte tu app de Nuxt a móvil en su lugar.

Requisitos

Asegúrate de tener instalado lo siguiente:

  • Node.js 18+ (comprueba con tu node --version)
  • Bun gestor de paquetes (curl -fsSL https://bun.sh/install | bash)
  • Xcode (solo macOS, para desarrollo iOS)
  • Android Studio (para desarrollo Android)

Paso 1: Crea un nuevo proyecto Nuxt 4

Empieza creando un proyecto Nuxt 4 desde cero:

bunx nuxi@latest init my-mobile-app
cd my-mobile-app
bun install

Estructura de directorios de Nuxt 4

Nuxt 4 usa una nueva estructura de directorios con la app code en el app/ directorio:

my-mobile-app/
  app/
    assets/
    components/
    composables/
    layouts/
    middleware/
    pages/
    plugins/
    utils/
    app.vue
  public/
  server/
  nuxt.config.ts
  package.json

Esta estructura ofrece una mejor separación entre la app y el servidor code.

Paso 2: Configurar Nuxt para generación estática

Capacitor requiere archivos HTML/JS/CSS estáticos. Configura Nuxt para la generación estática en nuxt.config.ts:

export default defineNuxtConfig({
  compatibilityDate: '2025-01-15',
  devtools: { enabled: true },

  // Enable static generation
  ssr: true,
  nitro: {
    preset: 'static',
  },
});

Paso 3: Añadir scripts móviles

Actualiza tu package.json con scripts de desarrollo móvil:

{
  "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"
  }
}

Prueba la generación estática:

bun run generate

Deberías ver un .output/public directorio con tus archivos estáticos.

Paso 4: Instalar Capacitor 8

Instala los paquetes core de Capacitor:

bun add @capacitor/core
bun add -D @capacitor/cli

Instala los plugins esenciales para la mayoría de las apps móviles:

bun add @capacitor/app @capacitor/keyboard @capacitor/splash-screen @capacitor/status-bar @capacitor/preferences

Qué hacen estos plugins:

  • @capacitor/app — Eventos del ciclo de vida de la app (foreground/background, deep links)
  • @capacitor/keyboard — Control del comportamiento del teclado
  • @capacitor/splash-screen — Control de la splash screen nativa
  • @capacitor/status-bar — Estilizar la barra de estado del dispositivo
  • @capacitor/preferences — Almacenamiento clave-valor (como localStorage pero nativo)

Paso 5: Inicializar Capacitor

Inicializa Capacitor con los detalles de tu proyecto:

bunx cap init "My Mobile App" com.example.mymobileapp --web-dir .output/public

Reemplaza:

  • "My Mobile App" con el nombre visible de tu app
  • com.example.mymobileapp con el ID de tu app (notación de dominio inverso)

Esto crea capacitor.config.ts. Actualízalo con la configuración del plugin:

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;

Paso 6: Añadir plataformas nativas

Instala los paquetes de la plataforma:

bun add @capacitor/ios @capacitor/android

Genera los proyectos nativos:

bunx cap add ios
bunx cap add android

Esto crea ios y android directorios que contienen los proyectos nativos.

Paso 7: Compilar y ejecutar

Compila tu proyecto y sincroniza con las plataformas nativas:

bun run mobile

Abrir en el simulador de iOS:

bun run mobile:ios

O en el emulador de Android:

bun run mobile:android

En Xcode (iOS):

  1. Selecciona un simulador del desplegable de dispositivos
  2. Haz clic en el botón Play o presiona Cmd + R

En Android Studio:

  1. Espera a que Gradle termine de sincronizar
  2. Selecciona un emulador en el desplegable de dispositivos
  3. Haz clic en el botón Run o presiona Shift + F10

Paso 8: Configura Live Reload

Para desarrollar más rápido, activa el live reload y los cambios se verán al instante en tu dispositivo.

  1. Busca tu dirección IP local:
# macOS
ipconfig getifaddr en0

# Windows
ipconfig
  1. Crea una config de desarrollo para Capacitor. Actualizar 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;
  1. Inicia el servidor de dev y copia la config al nativo:
bun run dev &
NODE_ENV=development bunx cap copy
  1. Reconstruye en Xcode/Android Studio

Ahora, los cambios en tu code de Nuxt tendrán hot-reload en el dispositivo.

Paso 9: Crea tu primera pantalla móvil

Creemos una pantalla de inicio optimizada para móviles. Actualizar app/app.vue:

<template>
  <NuxtPage />
</template>

Crear app/pages/index.vue:

<template>
  <main
    class="min-h-screen bg-linear-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>

Paso 10: Añadir Tailwind CSS

Para que los estilos funcionen, añade Tailwind CSS a tu proyecto:

bun add tailwindcss @tailwindcss/vite

Actualizar 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()],
  },
});

Crear 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;
}

Paso 11: Añadir el plugin Share

Implementemos la funcionalidad del botón de compartir:

bun add @capacitor/share

Actualizar app/pages/index.vue para usar el plugin Share:

<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>

Sincronizar y reconstruir:

bun run mobile

Estructura del proyecto

Tu proyecto debería verse así ahora:

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
└── ...

Próximos pasos

Ya tienes una app móvil con Nuxt funcionando. Esto es lo que sigue:

Configuración esencial

  • Iconos de la app: Reemplaza los iconos por defecto en ios/App/App/Assets.xcassets y android/app/src/main/res
  • Pantalla splash: Personaliza en proyectos nativos o usa @capacitor/splash-screen configuración
  • Deep Links: Configura los URL schemes de tu app

Añade más funciones

  • Cámara: bun add @capacitor/camera
  • Geolocalización: bun add @capacitor/geolocation
  • Notificaciones push: bun add @capacitor/push-notifications o @capgo/capacitor-firebase-messaging para Firebase Cloud Messaging en iOS y Android
  • Sistema de archivos: bun add @capacitor/filesystem

UI y transiciones nativas

Usa plugins de Capgo en vez de Konsta UI para una sensación nativa en móvil:

bun add @capgo/capacitor-native-navigation @capgo/capacitor-transitions
bunx cap sync

Para safe areas con Tailwind, añade @capgo/tailwind-capacitor:

bun add -D tailwind-capacitor

Ver Uso de @capgo/capacitor-native-navigation, Uso de @capgo/capacitor-transitions, y la repo tailwind-capacitor para la configuración de Nuxt.

Cómo solucionar problemas de layout en iOS (viewport, safe area y overflow horizontal)

Si el contenido se ve recortado, desplazado o con scroll horizontal en iOS, añadir más overflow-x: hidden o ajustar solo la etiqueta viewport no suele solucionarlo. Revisa estas comprobaciones en orden.

Asegúrate de que la meta tag viewport esté bien aplicada

En nuxt.config.ts, configura el viewport mediante app.head:

export default defineNuxtConfig({
  app: {
    head: {
      meta: [
        {
          name: 'viewport',
          content: 'width=device-width, initial-scale=1, viewport-fit=cover',
        },
      ],
    },
  },
});

Gestiona el safe area de iOS desde un único wrapper raíz

Crea un único app shell y aplica el padding de safe area ahí, no en múltiples componentes anidados:

html,
body,
#__nuxt {
  width: 100%;
  min-height: 100%;
  margin: 0;
  padding: 0;
  overflow-x: hidden;
}

* {
  box-sizing: border-box;
}

.app-shell {
  min-height: 100dvh;
  width: 100%;
  padding-top: env(safe-area-inset-top);
  padding-right: env(safe-area-inset-right);
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: env(safe-area-inset-left);
}

Envuelve todo el contenido de la página dentro de .app-shell. El padding de safe-area duplicado en headers, modales y wrappers de layout suele hacer que la UI parezca recortada o demasiado grande.

Con @capgo/tailwind-capacitor, puedes aplicar el mismo padding usando utilidades como pt-safe pb-safe px-safe en ese único shell.

Configura Capacitor iOS contentInset a never primero

En capacitor.config.ts, prefiere desactivar el inset nativo y dejar que CSS (o la navegación nativa) contentInsetMode: 'css') se encargue del safe area:

const config: CapacitorConfig = {
  appId: 'com.example.myapp',
  appName: 'my-app',
  webDir: '.output/public',
  ios: {
    contentInset: 'never',
  },
};

Combinar el content inset automático de Capacitor con CSS env(safe-area-inset-*) es una causa común de espaciado doble.

Encuentra el elemento que realmente desborda

El culpable habitual es un elemento que usa 100vw, Tailwind w-screen, un ancho fijo en píxeles, o un min-width.

En Safari Web Inspector, ejecuta:

[...document.querySelectorAll('*')]
  .filter(el => el.scrollWidth > document.documentElement.clientWidth)
  .map(el => ({
    el,
    tag: el.tagName,
    class: el.className,
    scrollWidth: el.scrollWidth,
    clientWidth: document.documentElement.clientWidth,
  }));

Con Tailwind, reemplaza w-screen con w-full siempre que sea posible. Muchos problemas de desbordamiento horizontal vienen de 100vw / w-screen, padding de safe-area duplicado o un contenedor de ancho fijo, no del propio meta tag del viewport.

Actualizaciones OTA

Configura Capgo para enviar actualizaciones sin reenviar la app a la tienda:

bunx @capgo/cli init

Solución de problemas

El build falla con “Cannot find module” Ejecuta bun install e inténtalo de nuevo.

iOS: “No se encontró ninguna identidad de firma” Abre Xcode, ve a Signing &amp; Capabilities y selecciona tu equipo de desarrollo.

Android: “No se encontró la ubicación de SDK” Crear android/local.properties con sdk.dir=/path/to/android/sdk

Los cambios no aparecen en el dispositivo Asegúrate de haber ejecutado bun run mobile tras realizar cambios. Para live reload, verifica que la IP sea correcta y el dev server esté corriendo.

.output/public está vacío o no existe Asegúrate de haber configurado nitro: { preset: 'static' } en nuxt.config.ts y ejecuta bun run generate.

Recursos

¿Listo para lanzar tu app? Descubre cómo Capgo te ayuda a entregar updates más rápido — crea una cuenta gratis hoy.

Continuar con Build a Nuxt Mobile App from Scratch with Capacitor 8

Si estás usando Crea una app móvil con Nuxt desde cero con Capacitor 8 para planificar la automatización de CI/CD, conéctalo con CI/CD de Capgo para el flujo de trabajo del producto en Capgo CI/CD, Native Builds de Capgo para el flujo de trabajo del producto en Capgo Native Builds, Integraciones de Capgo para el flujo de trabajo del producto en Capgo Integraciones, Integración CI/CD para los detalles de implementación de CI/CD Integration, y Integración con GitHub Actions para los detalles de implementación de GitHub Actions Integration.

Live updates para apps Capacitor

Cuando un bug de la capa web está en producción, publica el fix a través de Capgo en lugar de esperar días a la aprobación de la app store. Los usuarios reciben la actualización en segundo plano mientras los cambios nativos siguen el flujo de revisión habitual.

Lo último de nuestro blog

Capgo te da la información clave que necesitas para crear una app móvil realmente profesional.