Use when adding new error messages to React, or seeing "unknown error code" warnings.
npx skills add Cap-go/capacitor-skills --skill "ionic-design"
Install specific skill from multi-skill repository
# Description
Guide to using Ionic Framework components for beautiful native-looking Capacitor apps. Covers component usage, theming, platform-specific styling, and best practices for mobile UI. Use this skill when users need help with Ionic components or mobile UI design.
# SKILL.md
name: ionic-design
description: Guide to using Ionic Framework components for beautiful native-looking Capacitor apps. Covers component usage, theming, platform-specific styling, and best practices for mobile UI. Use this skill when users need help with Ionic components or mobile UI design.
Ionic Framework Design Guide
Build beautiful, native-looking mobile apps with Ionic Framework and Capacitor.
When to Use This Skill
- User is using Ionic components
- User wants native-looking UI
- User asks about Ionic theming
- User needs mobile UI patterns
- User wants platform-specific styling
What is Ionic Framework?
Ionic provides:
- 100+ mobile-optimized UI components
- Automatic iOS/Android platform styling
- Built-in dark mode support
- Accessibility out of the box
- Works with React, Vue, Angular, or vanilla JS
Getting Started
Installation
# For React
bun create vite my-app --template react-ts
cd my-app
bun add @ionic/react @ionic/react-router
# For Vue
bun create vite my-app --template vue-ts
cd my-app
bun add @ionic/vue @ionic/vue-router
# Add Capacitor
bun add @capacitor/core @capacitor/cli
bunx cap init
Setup (React)
// main.tsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import { IonApp, setupIonicReact } from '@ionic/react';
import App from './App';
/* Core CSS required for Ionic components to work properly */
import '@ionic/react/css/core.css';
/* Basic CSS for apps built with Ionic */
import '@ionic/react/css/normalize.css';
import '@ionic/react/css/structure.css';
import '@ionic/react/css/typography.css';
/* Optional CSS utils */
import '@ionic/react/css/padding.css';
import '@ionic/react/css/float-elements.css';
import '@ionic/react/css/text-alignment.css';
import '@ionic/react/css/text-transformation.css';
import '@ionic/react/css/flex-utils.css';
import '@ionic/react/css/display.css';
/* Theme */
import './theme/variables.css';
setupIonicReact();
const root = createRoot(document.getElementById('root')!);
root.render(
<IonApp>
<App />
</IonApp>
);
Core Components
Page Structure
import {
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonButtons,
IonBackButton,
} from '@ionic/react';
function MyPage() {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton defaultHref="/home" />
</IonButtons>
<IonTitle>Page Title</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
{/* Large title for iOS */}
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">Page Title</IonTitle>
</IonToolbar>
</IonHeader>
{/* Page content */}
<div className="ion-padding">
Your content here
</div>
</IonContent>
</IonPage>
);
}
Lists
import {
IonList,
IonItem,
IonLabel,
IonNote,
IonAvatar,
IonIcon,
IonItemSliding,
IonItemOptions,
IonItemOption,
} from '@ionic/react';
import { chevronForward, trash, archive } from 'ionicons/icons';
function ContactList() {
return (
<IonList>
{/* Simple item */}
<IonItem>
<IonLabel>Simple Item</IonLabel>
</IonItem>
{/* Item with detail */}
<IonItem detail button>
<IonLabel>
<h2>Item Title</h2>
<p>Item description text</p>
</IonLabel>
<IonNote slot="end">Note</IonNote>
</IonItem>
{/* Item with avatar */}
<IonItem>
<IonAvatar slot="start">
<img src="/avatar.jpg" alt="" />
</IonAvatar>
<IonLabel>
<h2>John Doe</h2>
<p>[email protected]</p>
</IonLabel>
</IonItem>
{/* Sliding item */}
<IonItemSliding>
<IonItem>
<IonLabel>Swipe me</IonLabel>
</IonItem>
<IonItemOptions side="end">
<IonItemOption color="danger">
<IonIcon slot="icon-only" icon={trash} />
</IonItemOption>
<IonItemOption>
<IonIcon slot="icon-only" icon={archive} />
</IonItemOption>
</IonItemOptions>
</IonItemSliding>
</IonList>
);
}
Forms
import {
IonInput,
IonTextarea,
IonSelect,
IonSelectOption,
IonToggle,
IonCheckbox,
IonRadioGroup,
IonRadio,
IonItem,
IonLabel,
IonButton,
} from '@ionic/react';
function MyForm() {
return (
<form>
{/* Text input */}
<IonItem>
<IonInput
label="Email"
labelPlacement="floating"
type="email"
placeholder="Enter email"
/>
</IonItem>
{/* Password */}
<IonItem>
<IonInput
label="Password"
labelPlacement="floating"
type="password"
/>
</IonItem>
{/* Textarea */}
<IonItem>
<IonTextarea
label="Bio"
labelPlacement="floating"
rows={4}
placeholder="Tell us about yourself"
/>
</IonItem>
{/* Select */}
<IonItem>
<IonSelect label="Country" placeholder="Select">
<IonSelectOption value="us">United States</IonSelectOption>
<IonSelectOption value="uk">United Kingdom</IonSelectOption>
<IonSelectOption value="de">Germany</IonSelectOption>
</IonSelect>
</IonItem>
{/* Toggle */}
<IonItem>
<IonToggle>Enable notifications</IonToggle>
</IonItem>
{/* Checkbox */}
<IonItem>
<IonCheckbox slot="start" />
<IonLabel>I agree to terms</IonLabel>
</IonItem>
{/* Radio group */}
<IonRadioGroup>
<IonItem>
<IonRadio value="small">Small</IonRadio>
</IonItem>
<IonItem>
<IonRadio value="medium">Medium</IonRadio>
</IonItem>
<IonItem>
<IonRadio value="large">Large</IonRadio>
</IonItem>
</IonRadioGroup>
<IonButton expand="block" type="submit">
Submit
</IonButton>
</form>
);
}
Buttons
import { IonButton, IonIcon } from '@ionic/react';
import { heart, share, download } from 'ionicons/icons';
function Buttons() {
return (
<>
{/* Fill variants */}
<IonButton>Solid</IonButton>
<IonButton fill="outline">Outline</IonButton>
<IonButton fill="clear">Clear</IonButton>
{/* Colors */}
<IonButton color="primary">Primary</IonButton>
<IonButton color="secondary">Secondary</IonButton>
<IonButton color="danger">Danger</IonButton>
<IonButton color="success">Success</IonButton>
{/* Sizes */}
<IonButton size="small">Small</IonButton>
<IonButton size="default">Default</IonButton>
<IonButton size="large">Large</IonButton>
{/* With icons */}
<IonButton>
<IonIcon slot="start" icon={heart} />
Like
</IonButton>
{/* Icon only */}
<IonButton>
<IonIcon slot="icon-only" icon={share} />
</IonButton>
{/* Full width */}
<IonButton expand="block">Block Button</IonButton>
<IonButton expand="full">Full Width</IonButton>
</>
);
}
Cards
import {
IonCard,
IonCardHeader,
IonCardTitle,
IonCardSubtitle,
IonCardContent,
IonImg,
IonButton,
} from '@ionic/react';
function Cards() {
return (
<IonCard>
<IonImg src="/card-image.jpg" alt="" />
<IonCardHeader>
<IonCardSubtitle>Card Subtitle</IonCardSubtitle>
<IonCardTitle>Card Title</IonCardTitle>
</IonCardHeader>
<IonCardContent>
Card content goes here. This is a standard card with
an image, title, subtitle, and content.
</IonCardContent>
<div className="ion-padding-horizontal ion-padding-bottom">
<IonButton fill="clear">Action 1</IonButton>
<IonButton fill="clear">Action 2</IonButton>
</div>
</IonCard>
);
}
Modals and Sheets
import { IonModal, IonButton, IonContent, IonHeader, IonToolbar, IonTitle } from '@ionic/react';
import { useState, useRef } from 'react';
function ModalExample() {
const [isOpen, setIsOpen] = useState(false);
const modal = useRef<HTMLIonModalElement>(null);
return (
<>
<IonButton onClick={() => setIsOpen(true)}>Open Modal</IonButton>
{/* Full page modal */}
<IonModal isOpen={isOpen} onDidDismiss={() => setIsOpen(false)}>
<IonHeader>
<IonToolbar>
<IonTitle>Modal Title</IonTitle>
<IonButton slot="end" onClick={() => setIsOpen(false)}>
Close
</IonButton>
</IonToolbar>
</IonHeader>
<IonContent>
<p>Modal content</p>
</IonContent>
</IonModal>
{/* Bottom sheet */}
<IonModal
ref={modal}
trigger="open-sheet"
initialBreakpoint={0.5}
breakpoints={[0, 0.25, 0.5, 0.75, 1]}
>
<IonContent>
<div className="ion-padding">
<h2>Sheet Content</h2>
<p>Drag to resize</p>
</div>
</IonContent>
</IonModal>
<IonButton id="open-sheet">Open Sheet</IonButton>
</>
);
}
Navigation
Tab Navigation
import {
IonTabs,
IonTabBar,
IonTabButton,
IonIcon,
IonLabel,
IonRouterOutlet,
} from '@ionic/react';
import { Route, Redirect } from 'react-router-dom';
import { home, search, person } from 'ionicons/icons';
function TabsLayout() {
return (
<IonTabs>
<IonRouterOutlet>
<Route exact path="/tabs/home" component={HomePage} />
<Route exact path="/tabs/search" component={SearchPage} />
<Route exact path="/tabs/profile" component={ProfilePage} />
<Route exact path="/tabs">
<Redirect to="/tabs/home" />
</Route>
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="home" href="/tabs/home">
<IonIcon icon={home} />
<IonLabel>Home</IonLabel>
</IonTabButton>
<IonTabButton tab="search" href="/tabs/search">
<IonIcon icon={search} />
<IonLabel>Search</IonLabel>
</IonTabButton>
<IonTabButton tab="profile" href="/tabs/profile">
<IonIcon icon={person} />
<IonLabel>Profile</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
);
}
Stack Navigation
import { IonReactRouter } from '@ionic/react-router';
import { IonRouterOutlet } from '@ionic/react';
import { Route } from 'react-router-dom';
function App() {
return (
<IonReactRouter>
<IonRouterOutlet>
<Route exact path="/" component={Home} />
<Route exact path="/detail/:id" component={Detail} />
</IonRouterOutlet>
</IonReactRouter>
);
}
Theming
Theme Variables
/* theme/variables.css */
:root {
/* Primary */
--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;
/* Secondary */
--ion-color-secondary: #3dc2ff;
/* Custom colors */
--ion-color-brand: #ff6b35;
--ion-color-brand-rgb: 255, 107, 53;
--ion-color-brand-contrast: #ffffff;
--ion-color-brand-shade: #e05e2f;
--ion-color-brand-tint: #ff7a49;
}
/* Dark mode */
@media (prefers-color-scheme: dark) {
:root {
--ion-background-color: #121212;
--ion-text-color: #ffffff;
--ion-color-step-50: #1e1e1e;
--ion-color-step-100: #2a2a2a;
}
}
/* iOS specific */
.ios {
--ion-toolbar-background: #f8f8f8;
}
/* Android specific */
.md {
--ion-toolbar-background: #ffffff;
}
Custom Component Styling
/* Global styles */
ion-content {
--background: var(--ion-background-color);
}
ion-card {
--background: #ffffff;
border-radius: 16px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
/* Platform-specific */
.ios ion-toolbar {
--border-width: 0;
}
.md ion-toolbar {
--border-width: 0 0 1px 0;
}
Platform-Specific Code
Detect Platform
import { isPlatform } from '@ionic/react';
// Check platform
if (isPlatform('ios')) {
// iOS-specific code
}
if (isPlatform('android')) {
// Android-specific code
}
if (isPlatform('hybrid')) {
// Running in native app
}
if (isPlatform('mobileweb')) {
// Running in mobile browser
}
Conditional Rendering
import { isPlatform, IonIcon } from '@ionic/react';
import { chevronBack, arrowBack } from 'ionicons/icons';
function BackButton() {
return (
<IonIcon
icon={isPlatform('ios') ? chevronBack : arrowBack}
/>
);
}
Best Practices
Performance
// Use IonVirtualScroll for long lists
import { IonVirtualScroll } from '@ionic/react';
<IonVirtualScroll
items={items}
renderItem={(item) => (
<IonItem key={item.id}>
<IonLabel>{item.name}</IonLabel>
</IonItem>
)}
/>
// Lazy load images
<IonImg src={url} /> // Automatically lazy loads
Accessibility
// Always provide labels
<IonButton aria-label="Delete item">
<IonIcon slot="icon-only" icon={trash} />
</IonButton>
// Use semantic elements
<IonItem button role="link">
<IonLabel>Clickable item</IonLabel>
</IonItem>
Safe Area
// Content respects safe areas by default
<IonContent>
{/* Auto padding for notch/home indicator */}
</IonContent>
// Custom safe area handling
<div style={{ paddingTop: 'env(safe-area-inset-top)' }}>
Custom header
</div>
Resources
- Ionic Documentation: https://ionicframework.com/docs
- Ionic Components: https://ionicframework.com/docs/components
- Ionicons: https://ionic.io/ionicons
- Color Generator: https://ionicframework.com/docs/theming/color-generator
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.