ionic-design ยท diff

git:20260318.5bd0bc2 to git:20261011.86834d4

60 added, 618 removed. Audit A to A.

---
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.
+ description: Builds and reviews Ionic Framework UI in Capacitor apps (Ionic 9 current, Ionic 8 still common) - page structure (`ion-header`/`ion-content`/`ion-footer`), lists, modern form syntax (`label`/`labelPlacement` on `ion-input`/`ion-select`), sheet modals, tabs + `ion-router-outlet`, CSS variable theming, dark palettes (`palettes/dark.system.css`, `.ion-palette-dark`), iOS vs MD mode, and Ionic 8 -> 9 breaking changes (React Router 6, Vue Router 5, Angular 18-22 zoneless, removed `ion-picker-legacy`, boolean `autocorrect`, `npx @ionic/migrate`). Use when writing Ionic components, theming an Ionic app, fixing platform-mode styling, or upgrading @ionic/react, @ionic/vue, @ionic/angular. Do not use for Konsta UI (konsta-ui), Tailwind setup (tailwind-capacitor), raw safe-area CSS (safe-area-handling), keyboard resize (capacitor-keyboard), or Ionic Appflow/Enterprise migrations (ionic-appflow-migration, ionic-enterprise-sdk-migration).
---
- # 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
-
- ```bash
- # For React
- npx create-vite@latest my-app --template react-ts
- cd my-app
- npm install @ionic/react @ionic/react-router
-
- # For Vue
- npx create-vite@latest my-app --template vue-ts
- cd my-app
- npm install @ionic/vue @ionic/vue-router
-
- # Add Capacitor
- npm install @capacitor/core @capacitor/cli
- npx cap init
- ```
-
- ### Setup (React)
-
- ```typescript
- // 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
-
- ```tsx
- 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
-
- ```tsx
- 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>john@example.com</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
-
- ```tsx
- 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
-
- ```tsx
- 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
-
- ```tsx
- 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
-
- ```tsx
- 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
-
- ```tsx
- 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
-
- ```tsx
- 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
-
- ```css
- /* 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
-
- ```css
- /* 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
-
- ```typescript
- 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
- }
+ # Ionic Framework Design
- if (isPlatform('mobileweb')) {
- // Running in mobile browser
- }
- ```
+ ## When to Use
- ### Conditional Rendering
+ TRIGGER when:
+ - Writing or reviewing `Ion*` / `ion-*` components in a Capacitor app
+ - Theming: `--ion-color-*`, dark mode, high contrast, per-platform styles
+ - iOS vs Material Design mode looks wrong, or `mode` overrides misbehave
+ - Upgrading Ionic 7/8 -> 9, or errors after `npm i @ionic/*@latest`
+ - Choosing Ionic vs another UI kit for a new Capacitor app
- ```tsx
- import { isPlatform, IonIcon } from '@ionic/react';
- import { chevronBack, arrowBack } from 'ionicons/icons';
+ Do not use:
+ - Konsta UI components: `konsta-ui`
+ - Tailwind v4 install / config: `tailwind-capacitor`
+ - Notch / system bar padding outside Ionic: `safe-area-handling`
+ - Keyboard covering inputs: `capacitor-keyboard` (Ionic apps use `resize: 'ionic'`)
+ - Leaving Appflow / Ionic Enterprise plugins: `ionic-appflow-migration`, `ionic-enterprise-sdk-migration`
- function BackButton() {
- return (
- <IonIcon
- icon={isPlatform('ios') ? chevronBack : arrowBack}
- />
- );
- }
- ```
+ ## Versions (October 2026)
- ## Best Practices
+ | Package | Current | Notes |
+ |---------|---------|-------|
+ | `@ionic/core`, `@ionic/react`, `@ionic/vue`, `@ionic/angular` | 9.x (`latest`) | 8.x on `v8-lts` tag |
+ | `ionicons` | 8.x | |
+ | Ionic 9 minimums | Capacitor 7+, iOS 16+, Android Chromium 89+, Angular 18-22, React 18/19 + React Router 6.4+, Vue 3.5 + vue-router 5, TypeScript 5.4+ | |
- ### Performance
+ Check installed versions first: `npm ls @ionic/core @ionic/react @ionic/vue @ionic/angular`. Write code for the installed major; do not mix v8 and v9 APIs.
- ```tsx
- // Use IonVirtualScroll for long lists
- import { IonVirtualScroll } from '@ionic/react';
+ ## Workflow
- <IonVirtualScroll
- items={items}
- renderItem={(item) => (
- <IonItem key={item.id}>
- <IonLabel>{item.name}</IonLabel>
- </IonItem>
- )}
- />
+ 1. **Inspect** framework, Ionic major, router version, global CSS imports, `setupIonicReact` / `IonicVue` / `provideIonicAngular` config, `capacitor.config` `Keyboard.resize`.
+ 2. **Upgrading?** Load [references/ionic-9-migration.md](references/ionic-9-migration.md); run `npx @ionic/migrate` in preview first, report findings, then apply.
+ 3. **Components**: [references/components.md](references/components.md) for page shell, lists, forms, overlays, tabs.
+ 4. **Theming**: [references/theming.md](references/theming.md) for colors, dark/high-contrast palettes, mode-specific CSS, shadow parts.
+ 5. **Verify** in both modes and on devices (below).
- // Lazy load images
- <IonImg src={url} /> // Automatically lazy loads
- ```
+ Only load a reference when its topic is in play.
- ### Accessibility
+ ## Rules that prevent most Ionic bugs
- ```tsx
- // Always provide labels
- <IonButton aria-label="Delete item">
- <IonIcon slot="icon-only" icon={trash} />
- </IonButton>
+ - Every routed page = `IonPage` > `IonHeader` + `IonContent` (+ optional `IonFooter`). Missing `IonPage` breaks transitions and lifecycle hooks.
+ - Scroll lives in `ion-content`. Do not make `body` or a wrapper `div` scroll; use `IonContent` `scrollToTop()`/`getScrollElement()`.
+ - Use `ion-router-outlet` (`IonRouterOutlet`, `IonTabs`) for URL routing. In Ionic 9 `ion-nav` no longer integrates with `ion-router`; keep it for local URL-less stacks inside a page.
+ - Safe areas are handled through `--ion-safe-area-top|right|bottom|left` (default `env()`); do not add body padding on top. Requires `viewport-fit=cover`.
+ - Forms: put `label` + `labelPlacement` on the control itself (`<IonInput label="Email" labelPlacement="floating" />`). Legacy `<IonItem><IonLabel/><IonInput/></IonItem>` labeling was removed in Ionic 8.
+ - Overlays: prefer inline `<IonModal isOpen>` / `trigger` or hooks (`useIonModal`, `modalController`) consistently; always handle `didDismiss` to reset state.
+ - Platform: style via `.ios` / `.md` classes on `html` or `mode` CSS selectors; use `isPlatform('ios')` only for behavior, never `navigator.userAgent`.
+ - Icons: import only used icons (`import { heart } from 'ionicons/icons'`) and pass `icon={heart}`; string names need `addIcons` registration in standalone/bundled builds.
- // Use semantic elements
- <IonItem button role="link">
- <IonLabel>Clickable item</IonLabel>
- </IonItem>
- ```
+ ## Verification
- ### Safe Area
+ 1. Build and sync: `npm run build && npx cap sync`.
+ 2. Run both modes in the browser: append `?ionic:mode=ios` and `?ionic:mode=md` to the dev URL; check headers, toolbars, lists, buttons.
+ 3. Toggle OS dark mode (or add `ion-palette-dark` to `<html>` if using the class palette); check contrast of custom colors.
+ 4. Device check: iPhone with Dynamic Island + Android 15/16 gesture nav for header/tab bar insets; open keyboard on a form page.
+ 5. After upgrade: `grep -rn "ion-picker-legacy\|pickerController\|useIonPicker\|@ionic/angular/standalone\|autocorrect=\"off\"" src/` returns nothing; `npx tsc --noEmit` passes.
- ```tsx
- // Content respects safe areas by default
- <IonContent>
- {/* Auto padding for notch/home indicator */}
- </IonContent>
+ ## Error Handling
- // Custom safe area handling
- <div style={{ paddingTop: 'env(safe-area-inset-top)' }}>
- Custom header
- </div>
- ```
+ | Error / symptom | Cause | Fix |
+ |-----------------|-------|-----|
+ | `Package path ./dist/... is not exported from package` (webpack) / `ERR_PACKAGE_PATH_NOT_EXPORTED` (Node) for `@ionic/core` | Ionic 9 `exports` map; deep import | Import from `@ionic/core`, `@ionic/core/components`, `@ionic/core/components/ion-*.js`, `@ionic/core/loader`, `@ionic/core/css/*.css` |
+ | Import errors or migrate warnings for `@ionic/angular/standalone` (Ionic 9) | Standalone imports moved | Import from `@ionic/angular`; lazy APIs from `@ionic/angular/lazy` |
+ | React: `Property 'component' does not exist` on `Route` / `IonRoute` | Ionic 9 requires React Router 6 | `element={<Page />}`, `Navigate` instead of `Redirect`, `useNavigate` instead of `useHistory` |
+ | Autocorrect turned ON after upgrade | `autocorrect="off"` string is truthy in Ionic 9 | Remove the attribute or bind `false` |
+ | `ionChange` no longer fires on re-selecting same option | Ionic 9 fires only on value change | Use `ionDismiss` for dismissal |
+ | Sheet modal handle now focusable / cycles breakpoints | Ionic 9 `handleBehavior` default `cycle` | `handle-behavior="none"` to restore |
+ | Styles missing, components unstyled | Core CSS not imported | Import `@ionic/<fw>/css/core.css` (+ structure/typography) once at entry |
+ | Dark mode never activates | No palette imported | Import `css/palettes/dark.system.css` (or `.class.css` + `ion-palette-dark` class) |
+ | Angular 21 view not updating after async work | Zoneless default in Ionic 9 + Angular 21 | Use signals / `markForCheck()`, or add `provideZoneChangeDetection()` |
## 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
+ - Ionic docs: https://ionicframework.com/docs
+ - Ionic 9 breaking changes: https://github.com/ionic-team/ionic-framework/blob/main/BREAKING.md
+ - Theming: https://ionicframework.com/docs/theming/basics
+ - Dark mode: https://ionicframework.com/docs/theming/dark-mode