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.
npm install -g @ionic/cli)# 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 syncsrc/
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.jsonion-button, ion-card, ion-list) for consistent cross-platform UI@capacitor/preferences for simple key-value storage (replaces @ionic/storage for most cases)isPlatform('hybrid') to check if running as native app vs PWAnpx cap sync after installing any Capacitor plugin or changing web assetstheme/variables.scssapp.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');
});
}
}
}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>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>pages/users/user-list/user-list.page.tsimport { 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');
}
}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!);
}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 });
}services/platform.service.tsimport { 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);
}
}services/storage.service.tsimport { 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();
}
}services/api.service.tsimport { 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 }));
}
}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.tsimport 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;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/user.model.tsexport 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;
};
}# 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@ionic/react or @ionic/vue.@capacitor/* 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.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.@angular/pwa for service worker support. Capacitor plugins gracefully degrade on web (some throw "not available" errors — wrap in isPluginAvailable checks).--ion-color-* variables for brand colors. Use @media (prefers-color-scheme: dark) for dark mode. Platform-specific overrides via .ios and .md CSS selectors.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.@capacitor-firebase/authentication + @capacitor-firebase/firestore. Configure via google-services.json (Android) and GoogleService-Info.plist (iOS).@capacitor-community/sqlite) with network detection via the Network API (@capacitor/network).achreftlili/deep-dev-skills
181fcbc
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.