CtrlK
BlogDocsLog inGet started
Tessl Logo

ionic-project-starter

Invalid
This skill can't be scored yet
Validation errors are blocking scoring. Review and fix them to unlock Quality, Impact and Security scores. See what needs fixing →
SKILL.md
Quality
Evals
Security

Ionic Project Starter

Scaffold a production-ready Ionic 8 app with Capacitor 6, Angular/React/Vue framework support, Ionic UI components, native plugins for Camera and Geolocation, platform detection, and theming.

Prerequisites

  • Node.js >= 20.x
  • npm >= 10.x
  • Ionic CLI (npm install -g @ionic/cli)
  • iOS: Xcode 16+ (macOS only), CocoaPods
  • Android: Android Studio with SDK 35+

Scaffold Command

# Angular (default)
ionic start <project-name> tabs --type=angular --capacitor
cd <project-name>

# React variant
ionic start <project-name> tabs --type=react --capacitor

# Vue variant
ionic start <project-name> tabs --type=vue --capacitor

# Add native platforms
ionic capacitor add ios
ionic capacitor add android

# Install common native plugins
npm install @capacitor/camera @capacitor/geolocation @capacitor/preferences
npm install @capacitor/haptics @capacitor/status-bar @capacitor/keyboard
npm install @capacitor/app @capacitor/splash-screen
npx cap sync

Project Structure

src/
  app/                             # Angular variant shown (React/Vue similar)
    app.module.ts                  # Root module
    app-routing.module.ts          # Route definitions
    app.component.ts               # Root component — platform setup
    tabs/
      tabs.page.ts                 # Tab bar layout
      tabs-routing.module.ts
    pages/
      home/
        home.page.ts               # Tab page
        home.page.html
        home.page.scss
      users/
        user-list/
          user-list.page.ts
          user-list.page.html
        user-detail/
          user-detail.page.ts
          user-detail.page.html
    components/
      user-card/
        user-card.component.ts     # Reusable component
        user-card.component.html
    services/
      api.service.ts               # HTTP client
      auth.service.ts              # Authentication
      user.service.ts              # User CRUD
      storage.service.ts           # Capacitor Preferences wrapper
      platform.service.ts          # Platform detection helpers
    guards/
      auth.guard.ts                # Route guard
    models/
      user.model.ts                # TypeScript interfaces
  theme/
    variables.scss                 # Ionic CSS variables (colors, fonts)
  global.scss                      # Global styles
  environments/
    environment.ts
    environment.prod.ts
capacitor.config.ts                # Capacitor configuration
ionic.config.json

Key Conventions

  • Ionic components (ion-button, ion-card, ion-list) for consistent cross-platform UI
  • Capacitor for native functionality — bridges web to iOS/Android native APIs
  • Platform-adaptive styling: Ionic auto-adjusts for iOS (Cupertino) vs Android (Material) look
  • Use @capacitor/preferences for simple key-value storage (replaces @ionic/storage for most cases)
  • Lazy-loaded route modules for each page/feature
  • Services for all data access — components are thin presentation layers
  • isPlatform('hybrid') to check if running as native app vs PWA
  • npx cap sync after installing any Capacitor plugin or changing web assets
  • Theming via CSS custom properties in theme/variables.scss

Essential Patterns

App Component with Platform Setup — app.component.ts (Angular)

import { Component, OnInit } from '@angular/core';
import { Platform } from '@ionic/angular';
import { StatusBar, Style } from '@capacitor/status-bar';
import { SplashScreen } from '@capacitor/splash-screen';
import { Keyboard } from '@capacitor/keyboard';
import { Capacitor } from '@capacitor/core';

@Component({
  selector: 'app-root',
  template: '<ion-app><ion-router-outlet></ion-router-outlet></ion-app>',
})
export class AppComponent implements OnInit {
  constructor(private platform: Platform) {}

  async ngOnInit() {
    await this.platform.ready();

    if (Capacitor.isNativePlatform()) {
      await StatusBar.setStyle({ style: Style.Light });
      await SplashScreen.hide();

      Keyboard.addListener('keyboardWillShow', (info) => {
        document.body.style.setProperty('--keyboard-height', `${info.keyboardHeight}px`);
      });
      Keyboard.addListener('keyboardWillHide', () => {
        document.body.style.setProperty('--keyboard-height', '0px');
      });
    }
  }
}

Tab Layout — tabs/tabs.page.html

<ion-tabs>
  <ion-tab-bar slot="bottom">
    <ion-tab-button tab="home" href="/tabs/home">
      <ion-icon name="home-outline"></ion-icon>
      <ion-label>Home</ion-label>
    </ion-tab-button>

    <ion-tab-button tab="users" href="/tabs/users">
      <ion-icon name="people-outline"></ion-icon>
      <ion-label>Users</ion-label>
    </ion-tab-button>

    <ion-tab-button tab="settings" href="/tabs/settings">
      <ion-icon name="settings-outline"></ion-icon>
      <ion-label>Settings</ion-label>
    </ion-tab-button>
  </ion-tab-bar>
</ion-tabs>

List Page — pages/users/user-list/user-list.page.html

<ion-header>
  <ion-toolbar>
    <ion-title>Users</ion-title>
    <ion-buttons slot="end">
      <ion-button (click)="addUser()">
        <ion-icon slot="icon-only" name="add"></ion-icon>
      </ion-button>
    </ion-buttons>
  </ion-toolbar>
  <ion-toolbar>
    <ion-searchbar
      [(ngModel)]="searchTerm"
      (ionInput)="onSearch($event)"
      placeholder="Search users..."
    ></ion-searchbar>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-refresher slot="fixed" (ionRefresh)="onRefresh($event)">
    <ion-refresher-content></ion-refresher-content>
  </ion-refresher>

  <ion-list *ngIf="users.length > 0; else emptyState">
    <app-user-card
      *ngFor="let user of filteredUsers"
      [user]="user"
      (click)="viewUser(user.id)"
    ></app-user-card>
  </ion-list>

  <ng-template #emptyState>
    <div class="ion-text-center ion-padding">
      <ion-icon name="people-outline" size="large" color="medium"></ion-icon>
      <p>No users found</p>
    </div>
  </ng-template>

  <ion-infinite-scroll (ionInfinite)="loadMore($event)">
    <ion-infinite-scroll-content></ion-infinite-scroll-content>
  </ion-infinite-scroll>
</ion-content>

List Page Logic — pages/users/user-list/user-list.page.ts

import { Component, OnInit } from '@angular/core';
import { NavController } from '@ionic/angular';
import { UserService } from '../../../services/user.service';
import { User } from '../../../models/user.model';

@Component({
  selector: 'app-user-list',
  templateUrl: './user-list.page.html',
  styleUrls: ['./user-list.page.scss'],
})
export class UserListPage implements OnInit {
  users: User[] = [];
  filteredUsers: User[] = [];
  searchTerm = '';
  private page = 1;

  constructor(
    private userService: UserService,
    private navCtrl: NavController,
  ) {}

  async ngOnInit() {
    await this.loadUsers();
  }

  async loadUsers() {
    this.users = await this.userService.getAll(this.page);
    this.filteredUsers = this.users;
  }

  onSearch(event: CustomEvent) {
    const query = (event.detail.value ?? '').toLowerCase();
    this.filteredUsers = this.users.filter(
      (u) => u.name.toLowerCase().includes(query) || u.email.toLowerCase().includes(query),
    );
  }

  async onRefresh(event: CustomEvent) {
    this.page = 1;
    await this.loadUsers();
    (event.target as HTMLIonRefresherElement).complete();
  }

  async loadMore(event: CustomEvent) {
    this.page++;
    const more = await this.userService.getAll(this.page);
    this.users.push(...more);
    this.filteredUsers = this.users;
    (event.target as HTMLIonInfiniteScrollElement).complete();

    if (more.length === 0) {
      (event.target as HTMLIonInfiniteScrollElement).disabled = true;
    }
  }

  viewUser(id: string) {
    this.navCtrl.navigateForward(`/tabs/users/${id}`);
  }

  addUser() {
    this.navCtrl.navigateForward('/tabs/users/new');
  }
}

Camera Plugin Usage

import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
import { Capacitor } from '@capacitor/core';

async takePhoto(): Promise<string | undefined> {
  // Check permissions
  const permissions = await Camera.checkPermissions();
  if (permissions.camera !== 'granted') {
    await Camera.requestPermissions();
  }

  const photo = await Camera.getPhoto({
    quality: 80,
    allowEditing: false,
    resultType: CameraResultType.Base64,
    source: CameraSource.Camera,
    width: 800,
  });

  return photo.base64String;
}

async pickFromGallery(): Promise<string | undefined> {
  const photo = await Camera.getPhoto({
    quality: 80,
    resultType: CameraResultType.Uri,
    source: CameraSource.Photos,
  });

  // Convert to web-usable URL
  return Capacitor.convertFileSrc(photo.path!);
}

Geolocation Plugin Usage

import { Geolocation, Position } from '@capacitor/geolocation';

async getCurrentPosition(): Promise<Position> {
  const permissions = await Geolocation.checkPermissions();
  if (permissions.location !== 'granted') {
    await Geolocation.requestPermissions();
  }

  return Geolocation.getCurrentPosition({
    enableHighAccuracy: true,
    timeout: 10000,
  });
}

watchPosition(callback: (position: Position) => void): Promise<string> {
  return Geolocation.watchPosition(
    { enableHighAccuracy: true },
    (position, err) => {
      if (position) callback(position);
      if (err) console.error('Geolocation error:', err);
    },
  );
}

async stopWatching(watchId: string) {
  await Geolocation.clearWatch({ id: watchId });
}

Platform Detection Service — services/platform.service.ts

import { Injectable } from '@angular/core';
import { Platform } from '@ionic/angular';
import { Capacitor } from '@capacitor/core';

@Injectable({ providedIn: 'root' })
export class PlatformService {
  constructor(private platform: Platform) {}

  /** Running as native app (iOS/Android) */
  get isNative(): boolean {
    return Capacitor.isNativePlatform();
  }

  /** Running in a browser (PWA or dev) */
  get isWeb(): boolean {
    return !this.isNative;
  }

  get isIOS(): boolean {
    return this.platform.is('ios');
  }

  get isAndroid(): boolean {
    return this.platform.is('android');
  }

  /** Check if a specific native plugin is available */
  isPluginAvailable(name: string): boolean {
    return Capacitor.isPluginAvailable(name);
  }
}

Storage Service — services/storage.service.ts

import { Injectable } from '@angular/core';
import { Preferences } from '@capacitor/preferences';

@Injectable({ providedIn: 'root' })
export class StorageService {
  async get<T>(key: string): Promise<T | null> {
    const { value } = await Preferences.get({ key });
    return value ? JSON.parse(value) : null;
  }

  async set<T>(key: string, value: T): Promise<void> {
    await Preferences.set({ key, value: JSON.stringify(value) });
  }

  async remove(key: string): Promise<void> {
    await Preferences.remove({ key });
  }

  async clear(): Promise<void> {
    await Preferences.clear();
  }
}

API Service — services/api.service.ts

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { environment } from '../../environments/environment';
import { StorageService } from './storage.service';

@Injectable({ providedIn: 'root' })
export class ApiService {
  private baseUrl = environment.apiUrl;

  constructor(
    private http: HttpClient,
    private storage: StorageService,
  ) {}

  private async getHeaders(): Promise<HttpHeaders> {
    const token = await this.storage.get<string>('auth_token');
    let headers = new HttpHeaders({ 'Content-Type': 'application/json' });
    if (token) {
      headers = headers.set('Authorization', `Bearer ${token}`);
    }
    return headers;
  }

  async get<T>(path: string): Promise<T> {
    const headers = await this.getHeaders();
    return firstValueFrom(this.http.get<T>(`${this.baseUrl}${path}`, { headers }));
  }

  async post<T>(path: string, body: unknown): Promise<T> {
    const headers = await this.getHeaders();
    return firstValueFrom(this.http.post<T>(`${this.baseUrl}${path}`, body, { headers }));
  }

  async put<T>(path: string, body: unknown): Promise<T> {
    const headers = await this.getHeaders();
    return firstValueFrom(this.http.put<T>(`${this.baseUrl}${path}`, body, { headers }));
  }

  async delete(path: string): Promise<void> {
    const headers = await this.getHeaders();
    await firstValueFrom(this.http.delete(`${this.baseUrl}${path}`, { headers }));
  }
}

Theming — theme/variables.scss

:root {
  // Primary brand color
  --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;

  // Custom app colors
  --app-background: #f5f5f5;
  --app-card-background: #ffffff;
  --app-text-primary: #1a1a2e;
  --app-text-secondary: #666666;
}

// Dark mode overrides
@media (prefers-color-scheme: dark) {
  :root {
    --ion-background-color: #1a1a2e;
    --ion-text-color: #ffffff;
    --app-background: #1a1a2e;
    --app-card-background: #2a2a3e;
    --app-text-primary: #ffffff;
    --app-text-secondary: #aaaaaa;
  }
}

// iOS-specific adjustments
.ios {
  --ion-toolbar-background: var(--ion-background-color);
}

// Android/Material-specific adjustments
.md {
  --ion-toolbar-background: var(--ion-color-primary);
  --ion-toolbar-color: var(--ion-color-primary-contrast);
}

Capacitor Config — capacitor.config.ts

import type { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  appId: 'com.example.myapp',
  appName: 'My App',
  webDir: 'www',
  server: {
    androidScheme: 'https',
  },
  plugins: {
    SplashScreen: {
      launchShowDuration: 2000,
      backgroundColor: '#ffffff',
      showSpinner: false,
    },
    StatusBar: {
      style: 'LIGHT',
    },
    Keyboard: {
      resize: 'body',
      resizeOnFullScreen: true,
    },
  },
};

export default config;

User Card Component — components/user-card/user-card.component.html

<ion-item detail>
  <ion-avatar slot="start">
    <ion-img [src]="user.avatarUrl || 'assets/default-avatar.svg'"></ion-img>
  </ion-avatar>
  <ion-label>
    <h2>{{ user.name }}</h2>
    <p>{{ user.email }}</p>
  </ion-label>
  <ion-note slot="end">
    {{ user.createdAt | date:'shortDate' }}
  </ion-note>
</ion-item>

Models — models/user.model.ts

export interface User {
  id: string;
  email: string;
  name: string;
  avatarUrl?: string;
  createdAt: string;
}

export interface CreateUserRequest {
  email: string;
  name: string;
  password: string;
}

export interface PaginatedResponse<T> {
  data: T[];
  meta: {
    page: number;
    perPage: number;
    total: number;
  };
}

Common Commands

# Development server (browser)
ionic serve

# Run on iOS simulator
ionic capacitor run ios --livereload --external

# Run on Android emulator
ionic capacitor run android --livereload --external

# Build web assets
ionic build
ionic build --prod          # Production build

# Sync web assets to native projects
npx cap sync

# Open native IDE
npx cap open ios
npx cap open android

# Add a new page
ionic generate page pages/settings

# Add a new component
ionic generate component components/header

# Add a new service
ionic generate service services/notification

# Update Capacitor plugins
npx cap update

# Check Capacitor doctor
npx cap doctor

# Build for production (iOS)
ionic build --prod && npx cap sync ios && npx cap open ios

# Build for production (Android)
ionic build --prod && npx cap sync android && npx cap open android

Integration Notes

  • Framework Choice: Ionic supports Angular, React, and Vue. Angular has the deepest integration (Ionic CLI generators, Angular-specific components). React and Vue use functional wrapper components from @ionic/react or @ionic/vue.
  • Capacitor vs Cordova: Capacitor is the modern runtime — use it for all new projects. It manages native projects as source (committed to git), not build artifacts. Access native APIs via @capacitor/* plugins.
  • Native Plugins: Official plugins (@capacitor/camera, @capacitor/geolocation, etc.) cover common needs. Community plugins on capacitor-community GitHub org. Custom plugins by extending Plugin class in Swift/Kotlin.
  • Platform Detection: Use Capacitor.isNativePlatform() to check native vs web. Use Platform.is('ios') / Platform.is('android') for platform-specific logic. Show/hide features based on platform capabilities.
  • PWA Support: Ionic apps are PWA-ready. Add @angular/pwa for service worker support. Capacitor plugins gracefully degrade on web (some throw "not available" errors — wrap in isPluginAvailable checks).
  • Theming: Ionic uses CSS custom properties. Override --ion-color-* variables for brand colors. Use @media (prefers-color-scheme: dark) for dark mode. Platform-specific overrides via .ios and .md CSS selectors.
  • Testing: Angular variant uses Jasmine + Karma (unit), Cypress or Playwright (E2E). React variant uses Jest + React Testing Library. Test native plugin calls by mocking Capacitor plugin imports.
  • App Store Deployment: Build web assets with ionic build --prod, sync with npx cap sync, then build/archive in Xcode (iOS) or Android Studio (Android). Alternatively, use Appflow for cloud builds and CI/CD.
  • Firebase: For authentication, Firestore, push notifications, and analytics, add the Firebase SDK: @capacitor-firebase/authentication + @capacitor-firebase/firestore. Configure via google-services.json (Android) and GoogleService-Info.plist (iOS).
  • Offline-First: For offline-first, use Capacitor Preferences or SQLite plugin (@capacitor-community/sqlite) with network detection via the Network API (@capacitor/network).
Repository
achreftlili/deep-dev-skills
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.