DEV Community

Ethan Walker
Ethan Walker

Posted on

Building Native Mobile Features with Capacitor and ionic-svelte in Svelte

ionic-svelte provides seamless integration between Ionic's mobile UI components and Svelte, enabling developers to build cross-platform mobile applications with native device capabilities. This article covers integrating Capacitor to access native device features like camera, geolocation, and push notifications. This is part 11 of a series on using ionic-svelte with Svelte.

This guide walks through creating a production-ready mobile application with native device integration using ionic-svelte, Capacitor, and Svelte, from initial setup to implementing real-world native features.

Prerequisites

Before starting, ensure you have:

  • A SvelteKit project (SvelteKit 1.0+ or standalone Svelte 4+)
  • Node.js 18+ and npm/pnpm/yarn
  • Basic understanding of Svelte components, stores, and lifecycle
  • Familiarity with mobile development concepts
  • Android Studio (for Android development) or Xcode (for iOS development) - optional but recommended for testing

Action example: The setupIonicBase() function initializes Ionic's base styles and configuration, making all Ionic components available throughout your Svelte application. The @ionic/core package provides web components that work seamlessly with Svelte's reactivity system.

Installation

Install ionic-svelte, Ionic Core, and Capacitor:

npm install @ionic/core ionic-svelte @capacitor/core @capacitor/cli
npm install @capacitor/ios @capacitor/android
Enter fullscreen mode Exit fullscreen mode

For specific native features, install the corresponding Capacitor plugins:

npm install @capacitor/camera @capacitor/geolocation @capacitor/push-notifications
npm install @capacitor/status-bar @capacitor/splash-screen
Enter fullscreen mode Exit fullscreen mode

Project Setup

First, configure your SvelteKit project to work with Ionic. Create the main layout file:

<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import { onMount } from 'svelte';
  import { setupIonicBase } from 'ionic-svelte';
  import '../theme/variables.css';
  import 'ionic-svelte/components/all';

  onMount(() => {
    setupIonicBase();
  });
</script>

<ion-app>
  <slot />
</ion-app>
Enter fullscreen mode Exit fullscreen mode

Disable server-side rendering in the layout server file:

// src/routes/+layout.ts
export const ssr = false;
Enter fullscreen mode Exit fullscreen mode

Create the Ionic theme variables file:

/* src/theme/variables.css */
:root {
  --ion-color-primary: #3880ff;
  --ion-color-primary-rgb: 56, 128, 255;
  --ion-color-primary-contrast: #ffffff;
  --ion-color-primary-contrast-rgb: 255, 255, 255;
  --ion-color-primary-shade: #3171e0;
  --ion-color-primary-tint: #4c8dff;

  --ion-color-secondary: #3dc2ff;
  --ion-color-secondary-rgb: 61, 194, 255;
  --ion-color-secondary-contrast: #ffffff;
  --ion-color-secondary-contrast-rgb: 255, 255, 255;
  --ion-color-secondary-shade: #36abe0;
  --ion-color-secondary-tint: #50c8ff;
}
Enter fullscreen mode Exit fullscreen mode

Initialize Capacitor in your project:

npx cap init
Enter fullscreen mode Exit fullscreen mode

This will prompt you for app details. Then add platforms:

npx cap add ios
npx cap add android
Enter fullscreen mode Exit fullscreen mode

Create the Capacitor configuration file:

// capacitor.config.ts
import { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  appId: 'com.example.app',
  appName: 'My Ionic Svelte App',
  webDir: 'build',
  server: {
    androidScheme: 'https'
  },
  plugins: {
    SplashScreen: {
      launchShowDuration: 2000,
      launchAutoHide: true,
      backgroundColor: "#3880ff",
      androidSplashResourceName: "splash",
      androidScaleType: "CENTER_CROP",
      showSpinner: true,
      androidSpinnerStyle: "large",
      iosSpinnerStyle: "small",
      spinnerColor: "#999999"
    }
  }
};

export default config;
Enter fullscreen mode Exit fullscreen mode

First Example / Basic Usage

Let's start with a simple example that checks if the app is running on a native platform:

<!-- src/routes/+page.svelte -->
<script lang="ts">
  import { onMount } from 'svelte';
  import { Capacitor } from '@capacitor/core';
  import { IonPage, IonHeader, IonToolbar, IonTitle, IonContent, IonButton } from 'ionic-svelte';

  let platform = 'web';
  let isNative = false;

  onMount(() => {
    platform = Capacitor.getPlatform();
    isNative = Capacitor.isNativePlatform();
  });
</script>

<IonPage>
  <IonHeader>
    <IonToolbar>
      <IonTitle>Native Features Demo</IonTitle>
    </IonToolbar>
  </IonHeader>

  <IonContent class="ion-padding">
    <h2>Platform: {platform}</h2>
    <p>Is Native: {isNative ? 'Yes' : 'No'}</p>

    <IonButton expand="block" color="primary">
      Get Started
    </IonButton>
  </IonContent>
</IonPage>
Enter fullscreen mode Exit fullscreen mode

This example demonstrates how to detect the platform and conditionally enable native features. The Capacitor.getPlatform() method returns 'ios', 'android', or 'web', while Capacitor.isNativePlatform() returns a boolean indicating if the app is running on a native device.

Understanding the Basics

Ionic components in Svelte work as web components, which means they integrate seamlessly with Svelte's reactivity. The ionic-svelte package provides Svelte-specific wrappers for complex components like navigation, tabs, and modals.

Key concepts:

  1. Platform Detection: Use Capacitor to detect the current platform and conditionally render features
  2. Plugin Access: Capacitor plugins provide access to native device APIs
  3. Lifecycle Management: Use Svelte's onMount and onDestroy to manage native resources

Here's a more advanced example showing platform-specific styling:

<!-- src/lib/PlatformAwareComponent.svelte -->
<script lang="ts">
  import { onMount } from 'svelte';
  import { Capacitor } from '@capacitor/core';
  import { IonButton } from 'ionic-svelte';

  let platform: string;
  let isIOS: boolean;
  let isAndroid: boolean;

  onMount(() => {
    platform = Capacitor.getPlatform();
    isIOS = platform === 'ios';
    isAndroid = platform === 'android';
  });
</script>

<div class="platform-container" class:ios={isIOS} class:android={isAndroid}>
  <IonButton color={isIOS ? 'primary' : 'secondary'}>
    Platform-Specific Button
  </IonButton>
</div>

<style>
  .platform-container.ios {
    --ion-color-primary: #007aff;
  }

  .platform-container.android {
    --ion-color-primary: #3880ff;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

Practical Example / Building Something Real

Let's build a complete feature that uses the camera, geolocation, and displays the data in an Ionic interface:

<!-- src/routes/camera-demo/+page.svelte -->
<script lang="ts">
  import { onMount } from 'svelte';
  import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
  import { Geolocation, Position } from '@capacitor/geolocation';
  import { 
    IonPage, 
    IonHeader, 
    IonToolbar, 
    IonTitle, 
    IonContent, 
    IonButton,
    IonCard,
    IonCardHeader,
    IonCardTitle,
    IonCardContent,
    IonImg,
    IonItem,
    IonLabel
  } from 'ionic-svelte';

  interface PhotoData {
    webPath: string;
    format: string;
  }

  interface LocationData {
    latitude: number;
    longitude: number;
    accuracy: number;
  }

  let photo: PhotoData | null = null;
  let location: LocationData | null = null;
  let loading = false;
  let error: string | null = null;

  async function takePicture() {
    try {
      loading = true;
      error = null;

      const image = await Camera.getPhoto({
        quality: 90,
        allowEditing: false,
        resultType: CameraResultType.Uri,
        source: CameraSource.Camera
      });

      photo = {
        webPath: image.webPath || '',
        format: image.format || 'jpeg'
      };
    } catch (err: any) {
      error = err.message || 'Failed to take picture';
      console.error('Camera error:', err);
    } finally {
      loading = false;
    }
  }

  async function getCurrentLocation() {
    try {
      loading = true;
      error = null;

      const position: Position = await Geolocation.getCurrentPosition({
        enableHighAccuracy: true,
        timeout: 10000
      });

      location = {
        latitude: position.coords.latitude,
        longitude: position.coords.longitude,
        accuracy: position.coords.accuracy || 0
      };
    } catch (err: any) {
      error = err.message || 'Failed to get location';
      console.error('Geolocation error:', err);
    } finally {
      loading = false;
    }
  }

  async function requestPermissions() {
    try {
      await Camera.requestPermissions();
      await Geolocation.requestPermissions();
    } catch (err: any) {
      error = 'Permission request failed: ' + err.message;
    }
  }

  onMount(() => {
    requestPermissions();
  });
</script>

<IonPage>
  <IonHeader>
    <IonToolbar>
      <IonTitle>Camera & Location Demo</IonTitle>
    </IonToolbar>
  </IonHeader>

  <IonContent class="ion-padding">
    {#if error}
      <IonCard color="danger">
        <IonCardHeader>
          <IonCardTitle>Error</IonCardTitle>
        </IonCardHeader>
        <IonCardContent>
          {error}
        </IonCardContent>
      </IonCard>
    {/if}

    <IonCard>
      <IonCardHeader>
        <IonCardTitle>Camera</IonCardTitle>
      </IonCardHeader>
      <IonCardContent>
        <IonButton 
          expand="block" 
          color="primary" 
          on:click={takePicture}
          disabled={loading}
        >
          {loading ? 'Processing...' : 'Take Picture'}
        </IonButton>

        {#if photo}
          <div class="photo-container">
            <IonImg src={photo.webPath} alt="Captured photo" />
          </div>
        {/if}
      </IonCardContent>
    </IonCard>

    <IonCard>
      <IonCardHeader>
        <IonCardTitle>Geolocation</IonCardTitle>
      </IonCardHeader>
      <IonCardContent>
        <IonButton 
          expand="block" 
          color="secondary" 
          on:click={getCurrentLocation}
          disabled={loading}
        >
          {loading ? 'Getting location...' : 'Get Current Location'}
        </IonButton>

        {#if location}
          <IonItem>
            <IonLabel>
              <h2>Latitude</h2>
              <p>{location.latitude.toFixed(6)}</p>
            </IonLabel>
          </IonItem>
          <IonItem>
            <IonLabel>
              <h2>Longitude</h2>
              <p>{location.longitude.toFixed(6)}</p>
            </IonLabel>
          </IonItem>
          <IonItem>
            <IonLabel>
              <h2>Accuracy</h2>
              <p>{location.accuracy.toFixed(2)} meters</p>
            </IonLabel>
          </IonItem>
        {/if}
      </IonCardContent>
    </IonCard>
  </IonContent>
</IonPage>

<style>
  .photo-container {
    margin-top: 1rem;
    display: flex;
    justify-content: center;
  }

  .photo-container :global(img) {
    max-width: 100%;
    height: auto;
    border-radius: 8px;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

This example demonstrates:

  • Camera Integration: Using Capacitor's Camera plugin to capture photos
  • Geolocation: Getting the device's current location with high accuracy
  • Permission Handling: Requesting necessary permissions on mount
  • Error Handling: Proper error states and user feedback
  • Ionic UI Components: Using Ionic cards, buttons, and layout components
  • Loading States: Managing async operations with loading indicators

Common Issues / Troubleshooting

Issue 1: Components not rendering correctly

Problem: Ionic components appear unstyled or don't work properly.

Solution: Ensure you've called setupIonicBase() in your layout and imported the CSS:

<script>
  import { setupIonicBase } from 'ionic-svelte';
  import '../theme/variables.css';
  import 'ionic-svelte/components/all';

  setupIonicBase();
</script>
Enter fullscreen mode Exit fullscreen mode

Issue 2: Native plugins return undefined on web

Problem: Camera or Geolocation plugins return undefined when testing in browser.

Solution: These plugins only work on native platforms. Use platform detection:

import { Capacitor } from '@capacitor/core';

if (Capacitor.isNativePlatform()) {
  // Use native plugin
} else {
  // Use web fallback or mock data
}
Enter fullscreen mode Exit fullscreen mode

Issue 3: Build errors when syncing with Capacitor

Problem: npx cap sync fails or build directory not found.

Solution: Ensure your build output directory matches webDir in capacitor.config.ts. For SvelteKit, update the config:

const config: CapacitorConfig = {
  webDir: '.svelte-kit/output/client', // or 'build' depending on your setup
  // ...
};
Enter fullscreen mode Exit fullscreen mode

Issue 4: Permissions not working on iOS/Android

Problem: Permission requests are ignored or fail silently.

Solution: Add permission descriptions to native configuration files. For iOS, edit ios/App/App/Info.plist. For Android, edit android/app/src/main/AndroidManifest.xml.

Next Steps

  • Explore more Capacitor plugins: Storage, Network, App, Haptics
  • Learn about Ionic's navigation system with IonNav and routing
  • Implement push notifications for real-time updates
  • Add biometric authentication using Capacitor's Biometric plugin
  • Optimize performance with lazy loading and code splitting
  • Check out the Ionic documentation for component APIs
  • Review Capacitor plugins for more native features

Summary

You've learned how to integrate native mobile features with ionic-svelte and Capacitor. You can now detect platforms, access device cameras and geolocation, handle permissions, and build production-ready mobile applications with Svelte. The combination of Ionic's UI components and Capacitor's native APIs provides a powerful foundation for cross-platform mobile development.

Top comments (0)