Agent skill
capacitor-ionic
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/capacitor-ionic
SKILL.md
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.
Fetching Current Documentation
Before providing version-specific guidance, verify current versions. Capacitor releases frequently, and plugin APIs evolve. Always verify current APIs before recommending specific patterns.
For library documentation, use WebSearch or install Context7 manually: claude plugin install context7@claude-plugins-official
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
appstore- Recommended: App Store Connect automation viaascCLI (TestFlight, submissions, metadata, signing)expo- Expo/React Native for comparisonreact-native-expert- Architecture and RN/Expo expertisemobile-testing- Testing Ionic/Capacitor appsdeep-linking-push- Deep linking and push in Capacitor
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?