ionic — Cursor rule: what it does, how to install
This rule guides Ionic work with TypeScript, covering component structure, typing, native async calls, and Ionic UI patterns.
.cursor/rules/ionic.mdcWhat ionic does
The rule sets conventions for Ionic components: keep each component in its own directory, use noun-based tags with a prefix, and order class members from internal state through rendering. Public @Prop(), @Event(), and @Method() members should carry JSDoc so their API is documented.
For TypeScript, it calls for noImplicitAny and strictNullChecks, while internal members should be marked private. It also covers awaiting Capacitor and Cordova calls, choosing Ionic components over custom UI where one exists, and using Ionic CSS variables for themes. Without noImplicitAny and strictNullChecks, a project misses a stated requirement of the rule.
The rule targets **/*.{js,jsx,ts,tsx}. Install it as .cursor/rules/ionic.mdc so Cursor can apply these conventions while editing JavaScript, JSX, TypeScript, and TSX files.
What it helps with
- organize Ionic components into separate directories
- enforce strict checks in
tsconfig.json - document public component APIs
- wait for Capacitor or Cordova results
- use Ionic UI components for navigation and controls
Example prompts for the agent
- Review this component against the Ionic rule and suggest structural fixes.
- Update the TypeScript code to satisfy
noImplicitAnyandstrictNullChecks. - Check that the
@capacitor/cameracall usesawaitand that public APIs have JSDoc.
How to install ionic
Manually: put the files in place
.cursor/rules/ionic.mdcFrontmatter: description, globs, alwaysApply
Manually: put the files in place
.claude/rules/ionic.mdThe paths frontmatter field limits the files: move globs from the source into paths
AGENTS.md
Codex reads AGENTS.md: paste the rule text there
Manually: put the files in place
.github/instructions/ionic.instructions.mdThe applyTo field sets which files it applies to: move globs from the source into applyTo
Manually: put the files in place
.devin/rules/ionic.mdManually: put the files in place
.clinerules/ionic.mdStep by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules
Questions about ionic
How do I install ionic in Cursor?
Place the rule file at .cursor/rules/ionic.mdc.
What does the ionic rule do?
It guides Ionic code structure, strict TypeScript settings, public API documentation, awaited native calls, Ionic UI usage, and CSS-variable theming.
Which files does the ionic rule apply to?
It applies to .js, .jsx, .ts, and .tsx files through **/*.{js,jsx,ts,tsx}.
Source text
ionic.mdc · CC0-1.0
---
description: This guide provides definitive best practices for developing high-performance, maintainable, and accessible Ionic applications using modern TypeScript and web standards.
globs: **/*.{js,jsx,ts,tsx}
---
# ionic Best Practices
Ionic Framework enables powerful cross-platform applications. Adhering to these guidelines ensures your codebase is robust, performant, and aligned with modern web development standards.
## 1. Code Organization and Structure
Adopt a consistent, component-centric file structure for clarity and maintainability.
### 1.1 One Component Per Directory
Each Ionic component (or Stencil component it wraps) must reside in its own directory, co-locating all related files.
❌ **BAD: Scattered files**
```
// components/my-button.tsx
// components/my-button.css
// components/my-card.tsx
// components/my-card.css
```
✅ **GOOD: Component-per-directory**
```
├── components
│ ├── ion-button
│ │ ├── ion-button.css
│ │ └── ion-button.tsx
│ └── ion-card
│ ├── ion-card.css
│ └── ion-card.tsx
```
### 1.2 Naming Conventions
Use descriptive, noun-based names for components, prefixed for uniqueness.
* **HTML Tag**: Use a unique, brand-specific prefix (e.g., `ion-`). Names should be nouns.
* **TS Class**: No prefix for the ES6 class name, as classes are scoped.
❌ **BAD: Verb-based or un-prefixed tags**
```tsx
// my-animating.tsx
@Component({ tag: 'animating-component' })
export class AnimatingComponent {}
```
✅ **GOOD: Noun-based, prefixed tags**
```tsx
// ion-animation.tsx
@Component({ tag: 'ion-animation' })
export class Animation {}
```
### 1.3 Component File Structure (Newspaper Metaphor)
Organize component class members logically, from high-level summaries to detailed implementations.
```typescript
@Component({
tag: 'ion-example',
styleUrls: {
ios: 'ion-example.ios.css',
md: 'ion-example.md.css',
},
})
export class IonExample {
/** 1. Internal Properties (not exposed) */
private internalValue: number = 0;
/** 2. Reference to host HTML element */
@Element() el!: HTMLElement;
/** 3. State() variables (internal reactive state) */
@State() isActive: boolean = false;
/** 4. Public Property API (@Prop()) with JSDocs */
/**
* The text to display on the button.
*/
@Prop() text: string = '';
/**
* If `true`, the button is disabled.
*/
@Prop() disabled: boolean = false;
/** 5. Prop lifecycle events (@Watch()) */
@Watch('disabled')
disabledChanged(newValue: boolean, oldValue: boolean) {
console.log(`Disabled changed from ${oldValue} to ${newValue}`);
}
/** 6. Events section (@Event()) with JSDocs */
/**
* Emitted when the button is clicked.
*/
@Event() ionClick!: EventEmitter<void>;
/** 7. Component lifecycle events (ordered by natural call order) */
connectedCallback() { /* ... */ }
disconnectedCallback() { /* ... */ }
componentWillLoad() { /* ... */ }
componentDidLoad() { /* ... */ }
componentWillRender() { /* ... */ }
componentDidRender() { /* ... */ }
/** 8. Listeners (@Listen()) */
@Listen('click', { target: 'document' })
onClick(event: Event) {
if (event.target === this.el) {
this.ionClick.emit();
}
}
/** 9. Public Methods (@Method()) with JSDocs */
/**
* Focuses the button.
*/
@Method()
async setFocus() {
this.el.focus();
}
/** 10. Private Methods (internal logic) */
private updateInternalState() {
this.internalValue++;
}
/** 11. Render function (JSX/TSX) */
render() {
return (
<button disabled={this.disabled} onClick={() => this.ionClick.emit()}>
{this.text}
</button>
);
}
}
```
## 2. TypeScript Best Practices
Leverage TypeScript for type safety, improved tooling, and maintainability.
### 2.1 Enforce Strict Typing
Always enable `noImplicitAny` and `strictNullChecks` in your `tsconfig.json`. Avoid `any` unless absolutely necessary.
❌ **BAD: Implicit `any`**
```typescript
function processData(data) { // data is implicitly 'any'
console.log(data.value);
}
```
✅ **GOOD: Explicit typing**
```typescript
interface Data {
value: string;
}
function processData(data: Data): void {
console.log(data.value);
}
```
### 2.2 Use `private` for Internal Members
Mark internal class properties and methods as `private` to enforce encapsulation and aid dead code detection.
❌ **BAD: All public by default**
```typescript
export class MyComponent {
helperMethod() { /* ... */ } // Public, but only used internally
}
```
✅ **GOOD: Encapsulated internals**
```typescript
export class MyComponent {
private helperMethod() { /* ... */ } // Clearly internal
}
```
### 2.3 JSDocs for Public API
Document all `@Prop()`, `@Event()`, and `@Method()` decorators with JSDoc comments to generate documentation and improve editor experience.
❌ **BAD: Undocumented public API**
```typescript
@Prop() value: string;
@Event() ionChange: EventEmitter<string>;
```
✅ **GOOD: Documented public API**
```typescript
/**
* The current value of the input.
*/
@Prop() value: string = '';
/**
* Emitted when the value changes.
*/
@Event() ionChange!: EventEmitter<string>;
```
## 3. Common Patterns and Anti-patterns
Adopt patterns that leverage Ionic's strengths and avoid common pitfalls.
### 3.1 Always `await` Asynchronous Native Plugin Calls
Capacitor/Cordova plugin calls are asynchronous. Always `await` their resolution to prevent race conditions and unexpected behavior.
❌ **BAD: Fire-and-forget native calls**
```typescript
import { Camera } from '@capacitor/camera';
async takePhoto() {
Camera.getPhoto({ /* ... */ }); // Missing await
console.log('Photo request sent'); // May log before photo is taken
}
```
✅ **GOOD: Await native calls**
```typescript
import { Camera, CameraResultType } from '@capacitor/camera';
async takePhoto() {
const photo = await Camera.getPhoto({
quality: 90,
allowEditing: false,
resultType: CameraResultType.Uri
});
console.log('Photo taken:', photo.webPath);
}
```
### 3.2 Prefer Ionic Components for UI
Leverage Ionic's rich set of UI components for adaptive styling, performance, and accessibility. Avoid custom implementations where an Ionic component exists.
❌ **BAD: Custom button for navigation**
```html
<button onclick="navigateTo('/home')">Home</button>
```
✅ **GOOD: Ionic button with router link**
```html
<ion-button routerLink="/home" routerDirection="forward">Home</ion-button>
```
### 3.3 Theming with CSS Variables
Customize your app's look and feel using Ionic's CSS Variables for consistent, platform-adaptive theming.
❌ **BAD: Hardcoded styles**
```css
.my-component {
background-color: #3880ff; /* Primary blue */
}
```
✅ **GOOD: CSS Variables for theming**
```css
.my-component {
background-color: var(--ion-color-primary);
}
```
## 4. Performance Considerations
Ionic is built for performance. Follow these to maintain a fast, responsive app.
### 4.1 Optimize DOM Updates
Avoid direct, heavy DOM manipulation. Let the framework (Angular, React, Vue, Stencil) manage DOM updates efficiently.
❌ **BAD: Manually updating styles on scroll**
```typescript
// In a scroll event listener
this.el.style.transform = `translateY(${scrollTop}px)`; // Triggers layout thrashing
```
✅ **GOOD: Use CSS transforms or Ionic's built-in scroll events**
```typescript
// Use CSS for animations or leverage Ionic's virtual scroll
// Or, if absolutely necessary, batch DOM reads/writes
```
### 4.2 Leverage Built-in Optimizations
Ionic components often include hardware-accelerated transitions, lazy loading, and tree-shaking. Ensure your build process enables these.
* **Tree-shaking**: Use modern bundlers (Webpack, Rollup) and ES Modules.
* **Lazy Loading**: For routes and components, especially in Angular/React/Vue.
## 5. Common Pitfalls and Gotchas
Be aware of these common issues to prevent bugs and performance regressions.
### 5.1 Ignoring Accessibility (a11y)
Ionic components are built with accessibility in mind, but developers must use them correctly. Run automated accessibility audits as part of CI.
❌ **BAD: Missing `aria-label` or semantic elements**
```html
<ion-icon name="menu" (click)="openMenu()"></ion-icon>
```
✅ **GOOD: Accessible elements**
```html
<ion-menu-button auto-hide="false"></ion-menu-button>
<!-- Or for a standalone icon that triggers an action -->
<ion-icon name="menu" aria-label="Open menu" (click)="openMenu()"></ion-icon>
```
### 5.2 Using Deprecated TSLint Configurations
TSLint is deprecated. Migrate to ESLint with `@stencil-community/eslint-plugin` and `eslint-config-ionic` for modern static analysis.
❌ **BAD: Relying on `tslint.json`**
```json
// tslint.json
{
"extends": "tslint-ionic-rules/strict"
}
```
✅ **GOOD: Using ESLint**
```json
// .eslintrc.js
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@stencil-community/recommended',
'@ionic/eslint-config/recommended'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', '@stencil-community'],
// ... other rules and configs
};
```
## 6. Testing Approaches
Implement a robust testing strategy for reliability and maintainability.
### 6.1 Unit Testing
Use Jest for unit testing individual components, services, and utilities. Focus on isolated logic.
```typescript
// my-service.spec.ts
import { MyService } from './my-service';
describe('MyService', () => {
let service: MyService;
beforeEach(() => {
service = new MyService();
});
it('should return the correct value', () => {
expect(service.calculate(2, 3)).toBe(5);
});
});
```
### 6.2 End-to-End (E2E) Testing
Use Playwright or Cypress for E2E tests to simulate user interactions across your application.
```javascript
// e2e/home.spec.ts (example with Playwright)
import { test, expect } from '@playwright/test';
test('should navigate to home and display title', async ({ page }) => {
await page.goto('/home');
await expect(page.locator('ion-title')).toHaveText('Home');
});
```
© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc