Capacitor / Ionic Expert
Comprehensive expertise in Capacitor 6+ and Ionic Framework for building cross-platform mobile applications from web technologies. Covers architecture, native API access, custom plugin development, deployment, and when to choose Capacitor over React Native or Flutter.
CRITICAL: Fetching Current Documentation
Before providing version-specific guidance, ALWAYS fetch the latest documentation:
// Capacitor
mcp__plugin_context7_context7__resolve-library-id({
libraryName: "@capacitor/core",
query: "Capacitor latest version features and plugin API"
});
// Ionic Framework
mcp__plugin_context7_context7__resolve-library-id({
libraryName: "@ionic/angular",
query: "Ionic Framework latest version components and features"
});
Why? Capacitor releases frequently, and plugin APIs evolve. Always verify current APIs before recommending specific patterns.
Capacitor 6+ Architecture
How Capacitor Works
┌──────────────────────────────────────────────────┐ │ Your Web App │ │ (Angular / React / Vue / Vanilla) │ ├──────────────────────────────────────────────────┤ │ Capacitor Bridge │ │ (Message passing between JS and Native) │ ├─────────────────────┬────────────────────────────┤ │ iOS (WKWebView) │ Android (WebView) │ │ Swift / ObjC │ Kotlin / Java │ │ Native APIs │ Native APIs │ └─────────────────────┴────────────────────────────┘
Key architectural principles:
- •Your web app runs in a native WebView (WKWebView on iOS, Android WebView)
- •Capacitor provides a bridge to call native APIs from JavaScript
- •Native code is a first-class citizen -- you own the Xcode and Android Studio projects
- •Plugins provide the abstraction layer between JS and native
Project Structure
my-app/ ├── src/ # Web app source (Angular/React/Vue) ├── ios/ # Native iOS project (Xcode) │ └── App/ │ ├── App/ │ │ ├── AppDelegate.swift │ │ ├── capacitor.config.json │ │ └── public/ # Built web assets copied here │ └── App.xcworkspace ├── android/ # Native Android project │ └── app/ │ ├── src/main/ │ │ ├── java/.../MainActivity.java │ │ └── assets/public/ # Built web assets copied here │ └── build.gradle.kts ├── capacitor.config.ts # Capacitor configuration ├── package.json └── ionic.config.json # If using Ionic CLI
Capacitor Configuration
// capacitor.config.ts
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.example.myapp',
appName: 'My App',
webDir: 'dist', // or 'www' for Angular
server: {
androidScheme: 'https', // Required for cookies, secure context
iosScheme: 'capacitor',
hostname: 'app.example.com',
},
plugins: {
SplashScreen: {
launchShowDuration: 2000,
launchAutoHide: true,
backgroundColor: '#ffffff',
androidSplashResourceName: 'splash',
showSpinner: false,
},
PushNotifications: {
presentationOptions: ['badge', 'sound', 'alert'],
},
Keyboard: {
resize: 'body',
resizeOnFullScreen: true,
},
},
ios: {
contentInset: 'automatic',
preferredContentMode: 'mobile',
},
android: {
allowMixedContent: false,
captureInput: true,
webContentsDebuggingEnabled: false, // Set true for debug builds
},
};
export default config;
Project Setup
# New Ionic + Capacitor project npm install -g @ionic/cli ionic start myApp tabs --type=angular --capacitor # Add Capacitor to existing web project npm install @capacitor/core @capacitor/cli npx cap init "My App" com.example.myapp --web-dir dist # Add platforms npx cap add ios npx cap add android # Build and sync npm run build npx cap sync # Copies web assets + updates native dependencies # Open native IDE npx cap open ios npx cap open android
Ionic Framework Integration
Angular Integration
// app.module.ts
import { IonicModule } from '@ionic/angular';
@NgModule({
imports: [
BrowserModule,
IonicModule.forRoot({
mode: 'ios', // Force iOS styling everywhere, or 'md' for Material
animated: true,
rippleEffect: true,
}),
AppRoutingModule,
],
})
export class AppModule {}
<!-- home.page.html -->
<ion-header>
<ion-toolbar>
<ion-title>Products</ion-title>
<ion-buttons slot="end">
<ion-button (click)="openCart()">
<ion-icon name="cart-outline" slot="icon-only"></ion-icon>
<ion-badge *ngIf="cartCount > 0">{{ cartCount }}</ion-badge>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" (ionRefresh)="refresh($event)">
<ion-refresher-content></ion-refresher-content>
</ion-refresher>
<ion-list>
<ion-item *ngFor="let product of products" (click)="viewProduct(product)">
<ion-thumbnail slot="start">
<img [src]="product.image" [alt]="product.name" />
</ion-thumbnail>
<ion-label>
<h2>{{ product.name }}</h2>
<p>{{ product.price | currency }}</p>
</ion-label>
<ion-button slot="end" fill="clear" (click)="addToCart(product, $event)">
<ion-icon name="add-circle-outline" slot="icon-only"></ion-icon>
</ion-button>
</ion-item>
</ion-list>
<ion-infinite-scroll (ionInfinite)="loadMore($event)">
<ion-infinite-scroll-content loadingSpinner="dots"></ion-infinite-scroll-content>
</ion-infinite-scroll>
</ion-content>
React Integration
// App.tsx
import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router';
setupIonicReact({ mode: 'ios' });
const App: React.FC = () => (
<IonApp>
<IonReactRouter>
<IonRouterOutlet>
<Route exact path="/home" component={Home} />
<Route exact path="/product/:id" component={ProductDetail} />
<Redirect exact from="/" to="/home" />
</IonRouterOutlet>
</IonReactRouter>
</IonApp>
);
// ProductList.tsx
import {
IonContent, IonHeader, IonList, IonItem, IonLabel,
IonRefresher, IonRefresherContent, IonSearchbar,
} from '@ionic/react';
const ProductList: React.FC = () => {
const [searchText, setSearchText] = useState('');
const [products, setProducts] = useState<Product[]>([]);
const handleRefresh = async (event: CustomEvent) => {
await fetchProducts();
event.detail.complete();
};
return (
<IonContent>
<IonSearchbar
value={searchText}
onIonInput={(e) => setSearchText(e.detail.value!)}
debounce={300}
/>
<IonRefresher slot="fixed" onIonRefresh={handleRefresh}>
<IonRefresherContent />
</IonRefresher>
<IonList>
{products.map((product) => (
<IonItem key={product.id} routerLink={`/product/${product.id}`}>
<IonLabel>
<h2>{product.name}</h2>
<p>${product.price}</p>
</IonLabel>
</IonItem>
))}
</IonList>
</IonContent>
);
};
Vue Integration
<!-- ProductList.vue -->
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Products</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item v-for="product in products" :key="product.id"
@click="router.push(`/product/${product.id}`)">
<ion-label>
<h2>{{ product.name }}</h2>
<p>{{ formatPrice(product.price) }}</p>
</ion-label>
</ion-item>
</ion-list>
</ion-content>
</ion-page>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useIonRouter } from '@ionic/vue';
const router = useIonRouter();
const products = ref<Product[]>([]);
onMounted(async () => {
products.value = await fetchProducts();
});
</script>
Native API Access
Core Capacitor Plugins
// Camera
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
async function takePhoto() {
const image = await Camera.getPhoto({
quality: 90,
allowEditing: true,
resultType: CameraResultType.Uri,
source: CameraSource.Camera,
});
return image.webPath;
}
// Geolocation
import { Geolocation } from '@capacitor/geolocation';
async function getCurrentPosition() {
const position = await Geolocation.getCurrentPosition({
enableHighAccuracy: true,
timeout: 10000,
});
return {
lat: position.coords.latitude,
lng: position.coords.longitude,
};
}
// Filesystem
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
async function writeFile(filename: string, data: string) {
await Filesystem.writeFile({
path: filename,
data,
directory: Directory.Documents,
encoding: Encoding.UTF8,
});
}
async function readFile(filename: string) {
const result = await Filesystem.readFile({
path: filename,
directory: Directory.Documents,
encoding: Encoding.UTF8,
});
return result.data;
}
// Haptics
import { Haptics, ImpactStyle } from '@capacitor/haptics';
async function vibrate() {
await Haptics.impact({ style: ImpactStyle.Medium });
}
// Share
import { Share } from '@capacitor/share';
async function shareContent() {
await Share.share({
title: 'Check this out',
text: 'Great product on My App',
url: 'https://example.com/product/123',
dialogTitle: 'Share with friends',
});
}
Platform Detection
import { Capacitor } from '@capacitor/core';
// Check if running natively
if (Capacitor.isNativePlatform()) {
// Use native APIs
} else {
// Fallback for web
}
// Get platform
const platform = Capacitor.getPlatform(); // 'ios' | 'android' | 'web'
// Check if plugin is available
if (Capacitor.isPluginAvailable('Camera')) {
const photo = await Camera.getPhoto({ ... });
} else {
// Fallback: use file input
}
Custom Capacitor Plugin Development
Plugin Structure
# Generate plugin scaffold npm init @capacitor/plugin my-custom-plugin
my-custom-plugin/ ├── src/ │ ├── definitions.ts # TypeScript interface │ ├── index.ts # Plugin registration │ └── web.ts # Web implementation ├── ios/ │ └── Sources/ │ └── MyCustomPlugin/ │ └── MyCustomPlugin.swift ├── android/ │ └── src/main/java/com/example/ │ └── MyCustomPlugin.java └── package.json
TypeScript Definition
// src/definitions.ts
export interface MyCustomPlugin {
echo(options: { value: string }): Promise<{ value: string }>;
getDeviceInfo(): Promise<DeviceInfo>;
startBackgroundTask(options: { interval: number }): Promise<void>;
addListener(
eventName: 'taskCompleted',
listenerFunc: (event: { result: string }) => void,
): Promise<PluginListenerHandle>;
}
export interface DeviceInfo {
model: string;
osVersion: string;
batteryLevel: number;
}
iOS Implementation (Swift)
import Capacitor
@objc(MyCustomPlugin)
public class MyCustomPlugin: CAPPlugin, CAPBridgedPlugin {
public let identifier = "MyCustomPlugin"
public let jsName = "MyCustomPlugin"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "echo", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getDeviceInfo", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "startBackgroundTask", returnType: CAPPluginReturnPromise),
]
@objc func echo(_ call: CAPPluginCall) {
let value = call.getString("value") ?? ""
call.resolve(["value": value])
}
@objc func getDeviceInfo(_ call: CAPPluginCall) {
let device = UIDevice.current
call.resolve([
"model": device.model,
"osVersion": device.systemVersion,
"batteryLevel": device.batteryLevel,
])
}
@objc func startBackgroundTask(_ call: CAPPluginCall) {
let interval = call.getInt("interval") ?? 60
DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(interval)) {
self.notifyListeners("taskCompleted", data: ["result": "done"])
}
call.resolve()
}
}
Android Implementation (Kotlin)
@CapacitorPlugin(name = "MyCustomPlugin")
class MyCustomPlugin : Plugin() {
@PluginMethod
fun echo(call: PluginCall) {
val value = call.getString("value") ?: ""
val result = JSObject()
result.put("value", value)
call.resolve(result)
}
@PluginMethod
fun getDeviceInfo(call: PluginCall) {
val result = JSObject()
result.put("model", Build.MODEL)
result.put("osVersion", Build.VERSION.RELEASE)
result.put("batteryLevel", getBatteryLevel())
call.resolve(result)
}
private fun getBatteryLevel(): Float {
val batteryManager = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
return batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY).toFloat() / 100
}
}
Live Reload and Development Workflow
# Start dev server with live reload on device
ionic cap run ios --livereload --external
ionic cap run android --livereload --external
# Or configure manually in capacitor.config.ts
const config: CapacitorConfig = {
server: {
url: 'http://192.168.1.100:5173', // Your dev server
cleartext: true, // Allow HTTP for dev
},
};
# Build and sync workflow
npm run build && npx cap sync
# Copy web assets only (faster, no native dep update)
npx cap copy
Capacitor vs Cordova Migration
Key Differences
| Feature | Cordova | Capacitor |
|---|---|---|
| Native project | Generated, gitignored | Owned, committed to git |
| Plugin system | XML config | Native code + npm |
| WebView | UIWebView (deprecated) | WKWebView / Modern WebView |
| CLI | cordova build | npx cap sync + Xcode/AS |
| Live reload | cordova run --livereload | cap run --livereload |
| PWA support | Separate build | Same codebase |
Migration Steps
# 1. Install Capacitor
npm install @capacitor/core @capacitor/cli
# 2. Initialize
npx cap init "My App" com.example.myapp --web-dir www
# 3. Add platforms
npx cap add ios
npx cap add android
# 4. Replace Cordova plugins with Capacitor equivalents
npm uninstall cordova-plugin-camera
npm install @capacitor/camera
# 5. Update plugin usage in code
// Before (Cordova):
// navigator.camera.getPicture(success, error, options);
// After (Capacitor):
import { Camera, CameraResultType } from '@capacitor/camera';
const photo = await Camera.getPhoto({ resultType: CameraResultType.Uri });
# 6. Build and test
npm run build && npx cap sync
npx cap open ios
Cordova Plugin Compatibility
# Many Cordova plugins still work in Capacitor npm install cordova-plugin-inappbrowser npx cap sync # Check compatibility npx cap doctor
Ionic UI Components and Theming
Custom Theming
/* theme/variables.css */
:root {
--ion-color-primary: #3880ff;
--ion-color-primary-rgb: 56, 128, 255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-shade: #3171e0;
--ion-color-primary-tint: #4c8dff;
--ion-color-secondary: #3dc2ff;
--ion-color-success: #2dd36f;
--ion-color-warning: #ffc409;
--ion-color-danger: #eb445a;
--ion-font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
--ion-background-color: #ffffff;
--ion-text-color: #1a1a1a;
}
/* Dark mode */
@media (prefers-color-scheme: dark) {
:root {
--ion-background-color: #121212;
--ion-text-color: #e0e0e0;
--ion-card-background: #1e1e1e;
--ion-toolbar-background: #1a1a1a;
}
}
PWA Support Alongside Native
Capacitor apps work as PWAs with zero extra configuration.
// Service worker registration (same build, web + native)
if ('serviceWorker' in navigator && !Capacitor.isNativePlatform()) {
navigator.serviceWorker.register('/sw.js');
}
// Adaptive behavior
function getStorageProvider() {
if (Capacitor.isNativePlatform()) {
return new CapacitorStorageProvider(); // Uses @capacitor/preferences
}
return new IndexedDBStorageProvider(); // Web fallback
}
Performance Optimization
WebView Performance
// capacitor.config.ts
const config: CapacitorConfig = {
android: {
// Enable hardware acceleration
allowMixedContent: false,
// Use Chrome Custom Tabs for external links
appendUserAgent: 'MyApp/1.0',
},
ios: {
preferredContentMode: 'mobile',
// Limiter for WKWebView
limitsNavigationsToAppBoundDomains: true,
},
};
Image and Asset Optimization
// Lazy load images with Ionic <ion-img [src]="product.image" alt="Product"></ion-img> // Use responsive images <picture> <source srcset="image-400.webp 400w, image-800.webp 800w" type="image/webp" /> <img src="image-800.jpg" loading="lazy" alt="Product" /> </picture>
Virtual Scrolling for Large Lists
<!-- Angular + Ionic virtual scroll -->
<ion-content>
<cdk-virtual-scroll-viewport itemSize="72" class="ion-content-scroll-host">
<ion-item *cdkVirtualFor="let item of items">
<ion-label>{{ item.name }}</ion-label>
</ion-item>
</cdk-virtual-scroll-viewport>
</ion-content>
Appflow (CI/CD) and Live Updates
# Install Appflow SDK
npm install @ionic/cloud
# Deploy live update
ionic deploy add --app-id YOUR_APP_ID --channel-name production
# Configure in capacitor.config.ts
plugins: {
LiveUpdates: {
appId: 'YOUR_APP_ID',
channel: 'production',
autoUpdateMethod: 'background',
maxVersions: 2,
},
}
Testing Strategies
Unit Testing (Angular Example)
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';
import { ProductListPage } from './product-list.page';
describe('ProductListPage', () => {
let component: ProductListPage;
let fixture: ComponentFixture<ProductListPage>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ProductListPage],
imports: [IonicModule.forRoot()],
providers: [{ provide: ProductService, useClass: MockProductService }],
}).compileComponents();
fixture = TestBed.createComponent(ProductListPage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('displays products after loading', async () => {
await component.ionViewDidEnter();
fixture.detectChanges();
const items = fixture.nativeElement.querySelectorAll('ion-item');
expect(items.length).toBeGreaterThan(0);
});
});
E2E with Cypress or Playwright
// Cypress test for Ionic app
describe('Product List', () => {
beforeEach(() => {
cy.visit('/');
cy.get('ion-content').should('be.visible');
});
it('loads and displays products', () => {
cy.get('ion-item').should('have.length.greaterThan', 0);
});
it('navigates to product detail', () => {
cy.get('ion-item').first().click();
cy.url().should('include', '/product/');
cy.get('ion-back-button').should('be.visible');
});
});
When to Use Capacitor vs React Native vs Flutter
| Factor | Capacitor/Ionic | React Native | Flutter |
|---|---|---|---|
| Best for | Web teams going mobile | Mobile-first with web skills | High-performance custom UI |
| UI rendering | Native WebView | Native components | Custom Skia rendering |
| Performance | Good (web perf) | Great | Excellent |
| Code sharing | 95%+ with web app | 70-85% iOS/Android | 80-95% iOS/Android |
| Team skills | HTML/CSS/JS | React + some native | Dart + some native |
| Existing web app | Reuse directly | Rewrite UI layer | Full rewrite |
| Native feel | Good with Ionic UI | Excellent | Excellent |
| Plugin ecosystem | Cordova + Capacitor | Large | Growing |
| Startup speed | Slower (WebView init) | Fast | Fast |
Choose Capacitor when:
- •You have an existing web application to mobilize
- •Your team is primarily web developers
- •You need a single codebase for web + iOS + Android
- •Performance requirements are standard (not gaming or heavy animation)
- •You want to ship quickly with familiar tools
Choose React Native when:
- •You need native UI feel and performance
- •Your team knows React
- •You need deep native integration
- •You are building a mobile-first product
Choose Flutter when:
- •You need pixel-perfect custom UI across platforms
- •Performance is critical (animations, transitions)
- •You want the tightest cross-platform consistency
- •You are starting from scratch with no web codebase
Related Skills
- •
expo- Expo/React Native for comparison - •
mobile-architect- Architecture decisions - •
mobile-testing- Testing Ionic/Capacitor apps - •
deep-linking-push- Deep linking and push in Capacitor