In the ever-evolving landscape of mobile app development, the lines between web and native are becoming increasingly blurred. With frameworks like Capacitor, developers can now leverage their existing web development skills to build high-quality, cross-platform mobile applications. This article will walk you through the process of building a mobile app with Next.js and Capacitor, using our “StoryCasts” app as a real-world example.

⚡ Why Choose Capacitor?
For years, developers have sought the “holy grail” of a single codebase that can power applications across the web, iOS, and Android. Capacitor brings us closer to that reality. Here’s why it’s a compelling choice:
- Leverage Your Existing Skills: If you’re a web developer, you can use your knowledge of HTML, CSS, and JavaScript (or TypeScript, in our case) to build mobile apps. There’s no need to learn Swift, Objective-C, Kotlin, or Java.
- Truly Native Access: Capacitor provides a powerful bridge to native APIs. This means you can access device features like the camera, GPS, and more, just as you would in a native application.
- Progressive Web App (PWA) Support: Capacitor apps are built on top of PWAs, so you can have a single codebase for your website, PWA, and mobile apps.
- Large and Active Community: Capacitor is an open-source project with a vibrant community, which means you’ll find plenty of resources, plugins, and support.
🌍 Building “StoryCasts”: A Practical Example
To illustrate the power of this stack, I’ve built StoryCasts
, an app that streams audio from YouTube.
Let’s dive into how it’s built.
📦 Project Setup
StoryCasts
app is built with Next.js, a popular React framework. The key to making Next.js work with
Capacitor is to configure it for a static export. This generates a set of HTML, CSS, and
JavaScript files that can be bundled into a native application.
Here’s a look at our next.config.ts
file:
import type { NextConfig } from "next";
const isMobile = process.env.NEXT_PUBLIC_IS_MOBILE === "true";
const nextConfig: NextConfig = {
...(isMobile ? { output: "export" } : {}),
reactStrictMode: true,
images: {
unoptimized: isMobile,
},
};
export default nextConfig;
When the NEXT_PUBLIC_IS_MOBILE
environment variable is set to 'true'
, we enable the output: 'export'
option.
We then configure Capacitor to use the out
directory (the default for Next.js static exports) as the webDir
.
Here is the capacitor.config.ts
:
import type { CapacitorConfig } from "@capacitor/cli";
import { KeyboardResize, KeyboardStyle } from "@capacitor/keyboard";
const config: CapacitorConfig = {
appId: "com.storycasts.app",
appName: "storycasts",
webDir: "out",
zoomEnabled: false,
plugins: {
Keyboard: {
resize: KeyboardResize.Body,
style: KeyboardStyle.Default,
resizeOnFullScreen: true,
},
SplashScreen: {
launchShowDuration: 0,
launchAutoHide: true,
launchFadeOutDuration: 0,
backgroundColor: "#000",
},
},
};
export default config;
📱 Responsive Design for All Screens
A key advantage of a web-based approach is the ability to create a responsive UI that works across all screen sizes. With Next.js and Tailwind CSS, we can easily build components that adapt to different viewports.
Our main page layout in src/app/[locale]/page.tsx
is a great example of this:
import { Separator } from '@/components/ui/separator';
import { Grid, Header, Player, Search, Tags } from '@/components';
import { MainLayout } from '@/layouts';
const Page = () => {
return (
<div className="min-h-screen transition-colors">
<Header />
<MainLayout>
<div className="space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center gap-4 container">
<Search />
</div>
<Tags />
<Separator className="my-6" />
<Grid />
</div>
</MainLayout>
<Player />
</div>
);
};
export default Page;
We use responsive utility classes from Tailwind CSS (like sm:flex-row
) to adjust the layout for different screen sizes.
🌐 Internationalization (i18n)
Next.js makes it easy to add internationalization to your app. We’ve structured our app with a [locale]
directory to handle different languages.
i18n configuration can be found in src/i18n/
, with message files for each supported language (e.g., en.json
and ar.json
).
Here’s a snippet from src/i18n/messages/en.json
:
{
"metadata": {
"title": "StoryCasts",
"description": "Stories you can hear.",
"author": "Muhammad Selim"
},
"locale": {
"title": "Locale Switcher",
"ar": "Arabic",
"en": "English"
},
"search": {
"placeholder": "Search stories, episodes, tags..."
},
"toggle_theme": "Toggle theme",
"now_playing": "Now Playing"
}
📱 Safe Area Handling
Modern mobile devices have notches, camera cutouts, and other “unsafe” areas where you shouldn’t render UI.
The capacitor-plugin-safe-area
makes it easy to handle this.
I’ve created a SafeAreaLayout
component that wraps our application and applies padding to avoid these areas.
Here’s the code for src/layouts/safe-area-layout.tsx
:
'use client';
import { SafeArea } from 'capacitor-plugin-safe-area';
import { SplashScreen } from '@capacitor/splash-screen';
import { useEffect, useState } from 'react';
import type { TChildrenProp } from '@/app/[locale]/types';
import { useStore } from '@/store';
export const SafeAreaLayout = ({ children }: TChildrenProp) => {
const [mounted, setMounted] = useState(false);
const { safeArea, setSafeArea } = useStore();
useEffect(() => {
const calculateSafeArea = async () => {
const { insets } = await SafeArea.getSafeAreaInsets();
const { statusBarHeight } = await SafeArea.getStatusBarHeight();
setSafeArea({ safeArea: { ...insets, statusBarHeight } });
setMounted(true);
await SplashScreen.hide();
};
calculateSafeArea();
}, [setSafeArea]);
if (!mounted || !safeArea) return null;
return <div className="overflow-hidden">{children}</div>;
};
This component fetches the safe area insets from the device and applies them to our layout.
🚀 Building and Running the App
To build and run the app, follow these steps:
- Install Dependencies: Make sure you have Node.js and npm installed. Then, run:
pnpm install # or npm install
- Scripts: Add the following scripts to your
package.json
:
"scripts": {
"sync": "NEXT_PUBLIC_IS_MOBILE=true next build --turbopack && npx cap sync",
"ios": "npx cap run ios",
"android": "npx cap run android",
"build": "next build --turbopack",
}
Now you can run the same project on both mobile and web.
🧩 Challenges to Consider
While Capacitor is a powerful tool, it’s not without its challenges:
- Performance: Web-based apps can sometimes be less performant than their native counterparts. For most apps, this difference is negligible, but for graphically intensive applications or games, you may want to consider a native approach.
- Plugin Dependency: You’ll rely on Capacitor plugins for native functionality. While there’s a large ecosystem of plugins available, you might encounter situations where a specific native feature isn’t available as a plugin.
Platform-Specific UI/UX
: iOS and Android have different design guidelines. While you can create a single UI that works on both platforms, you may want to consider platform-specific styling to make your app feel more “native.”
🚫 When Not to Use Capacitor
Capacitor is a great choice for many apps, but it’s not the right fit for every project. Here are a few scenarios where you might want to consider a different approach:
High-Performance Games
: For games that require a lot of processing power and low-level graphics access, a native approach or a game engine like Unity or Unreal Engine is a better choice.Apps with Heavy Background Processing
: If your app needs to perform complex tasks in the background, a native implementation will likely be more reliable and efficient.Apps Requiring Cutting-Edge Native Features
: If your app’s core functionality relies on a brand new native API that isn’t yet available as a Capacitor plugin, you’ll either need to build the plugin yourself or go with a native approach.
Conclusion
Building mobile apps with Next.js and Capacitor is a powerful and efficient way to leverage your web development skills to create cross-platform experiences. By following the principles and techniques outlined in this article, and by learning from our “StoryCasts” example, you’ll be well on your way to building your own amazing mobile apps.
Resources
Repo
You can find the complete code for the StoryCasts
app on GitHub: link