Fix: Capacitor Not Working — Build Failing, Plugins Not Loading, or Native Features Not Available
Quick Answer
How to fix Capacitor issues — project setup with Ionic or standalone, native plugin access, iOS and Android build errors, live reload, deep links, push notifications, and migration from Cordova.
The Problem
A Capacitor plugin call returns an error:
import { Camera, CameraResultType } from '@capacitor/camera';
const photo = await Camera.getPhoto({
resultType: CameraResultType.Base64,
});
// Error: "Camera" plugin is not implemented on webOr the native project doesn’t build:
npx cap sync
npx cap open ioserror: Unable to load contents of file list: '.../Build/Pods-App.xcfilelist'Or cap sync fails with mismatched versions:
[error] @capacitor/[email protected] and @capacitor/[email protected] are incompatible.
Upgrade to matching versions.Or live reload doesn’t connect to the device:
[error] Unable to connect to server at http://localhost:3000Why This Happens
Capacitor wraps a web app inside a native WebView and provides a bridge to native APIs. The integration has several failure points:
- Plugins have three implementations: web, iOS, Android — when running in the browser, only the web implementation is available. The Camera, Filesystem, and other device-specific plugins only work on actual devices or simulators. Calling them on the web throws “not implemented” unless the plugin has a web fallback.
- Native projects must be in sync with the web project —
cap synccopies the web build output into the native projects and updates native dependencies. Skipping this after adding a plugin or changing the config results in mismatches. - Version alignment is required —
@capacitor/core,@capacitor/ios,@capacitor/android, and all@capacitor/*plugins must be on the same major version. Mixing v4 and v5 packages causes runtime errors. - Live reload requires the device to reach the dev server — the device needs network access to the machine running the dev server.
localhostdoesn’t work from a physical device — use the machine’s local IP. Firewalls and network isolation often block the connection.
Fix 1: Set Up a Capacitor Project
# Add Capacitor to an existing web project
npm install @capacitor/core @capacitor/cli
npx cap init
# Add platforms
npm install @capacitor/ios @capacitor/android
npx cap add ios
npx cap add android// capacitor.config.ts
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.example.myapp',
appName: 'My App',
webDir: 'dist', // Where your web build output goes (dist, build, out)
server: {
// For live reload during development
// url: 'http://192.168.1.100:3000', // Your machine's local IP
// cleartext: true, // Allow HTTP (not HTTPS)
},
plugins: {
SplashScreen: {
launchAutoHide: true,
launchShowDuration: 2000,
backgroundColor: '#ffffff',
},
StatusBar: {
style: 'dark',
},
Keyboard: {
resize: 'body',
resizeOnFullScreen: true,
},
},
ios: {
contentInset: 'automatic',
scheme: 'My App',
},
android: {
buildOptions: {
keystorePath: undefined,
keystoreAlias: undefined,
},
},
};
export default config;Build and sync workflow:
# 1. Build your web app
npm run build
# 2. Copy web assets + sync native plugins
npx cap sync
# 3. Open in IDE
npx cap open ios # Opens Xcode
npx cap open android # Opens Android Studio
# Or run directly (Capacitor 5+)
npx cap run ios --target="iPhone 15"
npx cap run androidFix 2: Use Plugins Correctly
Install each plugin and sync:
npm install @capacitor/camera @capacitor/filesystem @capacitor/geolocation @capacitor/haptics @capacitor/share @capacitor/local-notifications
npx cap sync// Check platform before calling native-only plugins
import { Capacitor } from '@capacitor/core';
// Platform detection
const isNative = Capacitor.isNativePlatform(); // true on iOS/Android
const platform = Capacitor.getPlatform(); // 'ios', 'android', 'web'
// Camera — with web fallback
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
async function takePhoto() {
// Check if the plugin is available
if (!Capacitor.isPluginAvailable('Camera')) {
// Fallback: use file input on web
return useFileInput();
}
// Request permissions first on native
const permissions = await Camera.requestPermissions();
if (permissions.camera !== 'granted') {
throw new Error('Camera permission denied');
}
const photo = await Camera.getPhoto({
resultType: CameraResultType.Uri,
source: CameraSource.Camera,
quality: 90,
width: 1024,
allowEditing: false,
});
return photo.webPath; // Usable in <img src="">
}
// Filesystem — read/write app storage
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
async function saveFile(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;
}
// Geolocation
import { Geolocation } from '@capacitor/geolocation';
async function getCurrentPosition() {
const permissions = await Geolocation.requestPermissions();
if (permissions.location !== 'granted') {
throw new Error('Location permission denied');
}
const position = await Geolocation.getCurrentPosition({
enableHighAccuracy: true,
timeout: 10000,
});
return {
lat: position.coords.latitude,
lng: position.coords.longitude,
};
}
// Share
import { Share } from '@capacitor/share';
async function shareContent() {
await Share.share({
title: 'Check this out',
text: 'Sharing from my app',
url: 'https://example.com',
dialogTitle: 'Share with friends',
});
}Fix 3: Fix iOS Build Errors
Common Xcode build failures and their fixes:
# 1. Pod install failed — missing or outdated CocoaPods
cd ios/App && pod install --repo-update
# If that fails:
sudo gem install cocoapods
pod repo update
pod install
# 2. Clean build folder when Xcode caches cause issues
# In Xcode: Product → Clean Build Folder (Cmd+Shift+K)
# Or from terminal:
cd ios && xcodebuild clean
# 3. Version mismatch — update all Capacitor packages
npm install @capacitor/core@latest @capacitor/ios@latest @capacitor/cli@latest
npx cap sync ios
# 4. Minimum iOS version — update in Xcode or Podfile
# ios/App/Podfile
platform :ios, '14.0' # Capacitor 5 requires iOS 14+
# 5. Signing issues — update Team in Xcode
# Xcode → App target → Signing & Capabilities → TeamiOS permissions — add to Info.plist:
<!-- ios/App/App/Info.plist -->
<!-- Camera -->
<key>NSCameraUsageDescription</key>
<string>We need camera access to take photos</string>
<!-- Photo Library -->
<key>NSPhotoLibraryUsageDescription</key>
<string>We need photo library access to select images</string>
<!-- Location -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location to show nearby places</string>
<!-- Microphone (for video recording) -->
<key>NSMicrophoneUsageDescription</key>
<string>We need microphone access for video recording</string>Fix 4: Fix Android Build Errors
# 1. Gradle sync failed — update Gradle wrapper
cd android && ./gradlew wrapper --gradle-version=8.4
# 2. SDK version mismatch
# android/variables.gradle
ext {
minSdkVersion = 22 // Capacitor 5 minimum
compileSdkVersion = 34
targetSdkVersion = 34
androidxActivityVersion = '1.8.0'
androidxAppCompatVersion = '1.6.1'
androidxCoordinatorLayoutVersion = '1.2.0'
androidxCoreVersion = '1.12.0'
androidxFragmentVersion = '1.6.2'
coreSplashScreenVersion = '1.0.1'
androidxWebkitVersion = '1.9.0'
junitVersion = '4.13.2'
androidxJunitVersion = '1.1.5'
androidxEspressoCoreVersion = '3.5.1'
}
# 3. Clean and rebuild
cd android && ./gradlew clean
npx cap sync androidAndroid permissions — add to AndroidManifest.xml:
<!-- android/app/src/main/AndroidManifest.xml -->
<manifest>
<!-- Internet (usually already there) -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- Camera -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- Location -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Storage (Android 12 and below) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application ...>
</manifest>Fix 5: Live Reload on Device
// capacitor.config.ts — development config
const config: CapacitorConfig = {
appId: 'com.example.myapp',
appName: 'My App',
webDir: 'dist',
server: {
// Use your machine's local IP — NOT localhost
url: 'http://192.168.1.100:5173',
cleartext: true, // Allow HTTP on Android
},
};# Find your machine's local IP
# macOS:
ipconfig getifaddr en0
# Windows:
ipconfig | findstr "IPv4"
# Linux:
hostname -IAutomated live reload setup:
# Run dev server accessible on network
npm run dev -- --host 0.0.0.0
# Then sync and run on device
npx cap sync
npx cap run ios --livereload --external
# --external automatically sets the server URL to your IPAndroid cleartext HTTP — required for live reload:
<!-- android/app/src/main/AndroidManifest.xml -->
<application
android:usesCleartextTraffic="true"
...>Fix 6: Push Notifications
npm install @capacitor/push-notifications
npx cap syncimport { PushNotifications } from '@capacitor/push-notifications';
import { Capacitor } from '@capacitor/core';
async function initPushNotifications() {
if (!Capacitor.isNativePlatform()) return;
// Request permission
const permission = await PushNotifications.requestPermissions();
if (permission.receive !== 'granted') {
console.warn('Push notification permission denied');
return;
}
// Register for push notifications
await PushNotifications.register();
// Get the device token
PushNotifications.addListener('registration', (token) => {
console.log('Push token:', token.value);
// Send token to your backend
fetch('/api/devices', {
method: 'POST',
body: JSON.stringify({ token: token.value, platform: Capacitor.getPlatform() }),
});
});
// Registration error
PushNotifications.addListener('registrationError', (error) => {
console.error('Push registration failed:', error);
});
// Notification received while app is in foreground
PushNotifications.addListener('pushNotificationReceived', (notification) => {
console.log('Foreground notification:', notification);
// Show in-app notification UI
});
// User tapped a notification
PushNotifications.addListener('pushNotificationActionPerformed', (action) => {
console.log('Notification tapped:', action.notification);
// Navigate to relevant screen
const data = action.notification.data;
if (data.screen === 'order') {
router.push(`/orders/${data.orderId}`);
}
});
}iOS — enable Push Notifications capability in Xcode and upload your APNs key to Firebase or your push service.
Android — add google-services.json to android/app/ from the Firebase console.
Still Not Working?
“Plugin not implemented on web” — this is expected. Most hardware plugins (Camera, Geolocation, Haptics) have no web implementation. Use Capacitor.isNativePlatform() to conditionally call native APIs and provide web fallbacks. Some plugins like @capacitor/preferences (formerly Storage) work on all platforms.
cap sync runs but the plugin doesn’t appear in the native project — the plugin’s native code must be registered. For iOS, run cd ios/App && pod install. For Android, the plugin should auto-register via Gradle. If it doesn’t, check that the plugin package is in package.json dependencies (not devDependencies).
App works in browser but white screen on device — the web build output isn’t being served. Check that webDir in capacitor.config.ts matches your build output directory. Run npm run build before npx cap sync. Also check the browser console in Safari (iOS) or Chrome DevTools (Android) for JavaScript errors.
Deep links don’t open the app — deep linking requires native configuration beyond Capacitor. For iOS, add Associated Domains in Xcode and host an apple-app-site-association file. For Android, add intent filters in AndroidManifest.xml and host an assetlinks.json file. Use @capacitor/app plugin to handle the incoming URL.
For related mobile issues, see Fix: Expo Not Working and Fix: React Native Reanimated Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Deno PermissionDenied — Missing --allow-read, --allow-net, and Other Flags
How to fix Deno PermissionDenied (NotCapable in Deno 2) errors — the right permission flags, path-scoped permissions, deno.json permission sets, and the Deno.permissions API.
Fix: Fastify Not Working — 404, Plugin Encapsulation, and Schema Validation Errors
How to fix Fastify issues — route 404 from plugin encapsulation, reply already sent, FST_ERR_VALIDATION, request.body undefined, @fastify/cors, hooks not running, and TypeScript type inference.
Fix: Expo Router Not Working — Routes Not Matching, Layout Nesting Wrong, or Deep Links Failing
How to fix Expo Router issues — file-based routing, layout routes, dynamic segments, tabs and stack navigation, modal routes, authentication flows, and deep linking configuration.
Fix: React Native Paper Not Working — Theme Not Applying, Icons Missing, or Components Unstyled
How to fix React Native Paper issues — PaperProvider setup, Material Design 3 theming, custom color schemes, icon configuration, dark mode, and Expo integration.