Comprehensive Angular ESLint configuration with TypeScript support, component/template rules, accessibility, and CSS linting. Built on @noneforge/eslint-config ESLint 9+ flat config with Angular 20+ best practices and modern reactive patterns.
- β¨ ESLint 9 Flat Config - Modern configuration format with better performance
- π― Angular 20+ Optimized - Specific rules for components, directives, pipes, and services
- π Accessibility First - Built-in a11y rules for templates with ARIA support
- β‘ Signals & Control Flow - Support for Angular's latest reactive features
- π¦ Full TypeScript Support - Extends @noneforge/eslint-config base with Angular specifics
- π¨ CSS/SCSS Linting - Integrated CSS validation for component styles
- π Standalone Components - Promotes modern Angular architecture patterns
- π§ Smart Detection - Different rules for tests, stories, schematics, and modules
- @noneforge/eslint-config - TypeScript/Javascript base configuration
- @noneforge/eslint-config-node - Node.js backend configuration
- Node.js >=20.19.0
- ESLint >=9.22.0
- TypeScript >=5.9.0
- Angular >=20.0.0
- RxJS >=7.4.0
npm install --save-dev @noneforge/eslint-config-angular eslint typescript
or with Yarn:
yarn add --dev @noneforge/eslint-config-angular eslint typescript
Create an eslint.config.js
file in your project root:
import config from '@noneforge/eslint-config-angular';
export default [
...config,
// Your custom rules here
];
import config from '@noneforge/eslint-config-angular';
export default [
...config,
{
rules: {
// Override or add custom rules
'@angular-eslint/component-selector': ['error', {
type: 'element',
prefix: 'my-app',
style: 'kebab-case',
}],
}
}
];
import config from '@noneforge/eslint-config-angular';
export default [
...config,
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
project: ['./apps/*/tsconfig.json', './libs/*/tsconfig.json'],
}
}
}
];
Modern Angular patterns and best practices:
- Standalone Components:
prefer-standalone
- Enforces standalone architecture - Signals:
prefer-signals
,no-uncalled-signals
- Reactive state management - Change Detection:
prefer-on-push-component-change-detection
- Performance optimization - Lifecycle:
contextual-lifecycle
,no-empty-lifecycle-method
,no-async-lifecycle-method
- Component Structure:
component-max-inline-declarations
- Limits inline templates/styles - Dependency Injection:
prefer-inject
- Modern DI patterns over constructor injection - Naming Conventions:
component-class-suffix
,directive-class-suffix
- Selectors: Enforces
app-
prefix with kebab-case for components, camelCase for directives
// β Legacy patterns
@Component({
selector: 'myComponent',
template: `...very long template...`,
standalone: false, // Explicitly false triggers the rule
changeDetection: ChangeDetectionStrategy.Default
})
export class MyComp {
constructor(private service: MyService) {}
value = 0;
}
// β
Modern Angular patterns
@Component({
selector: 'app-my-component',
templateUrl: './my-component.html',
changeDetection: ChangeDetectionStrategy.OnPush
// standalone: true is default in Angular 20, can be omitted
})
export class MyComponent {
private service = inject(MyService);
value = signal(0);
}
Comprehensive template validation and best practices:
- Control Flow:
prefer-control-flow
- New @if/@for/@switch syntax - Two-Way Binding:
banana-in-box
- Correct [(ngModel)] syntax - Type Safety:
no-any
- Prevents any types in templates - Performance:
use-track-by-function
- Required for @for loops - Image Optimization:
prefer-ngsrc
- NgOptimizedImage for better performance - Self-Closing Tags:
prefer-self-closing-tags
- Cleaner template syntax - Complexity:
conditional-complexity
,cyclomatic-complexity
- Maintainable templates
<!-- β Old patterns -->
<div *ngIf="condition">Content</div>
<img src="{{imageUrl}}" />
<div *ngFor="let item of items">{{item}}</div>
<!-- β
Modern patterns -->
@if (condition) {
<div>Content</div>
}
<img [ngSrc]="imageUrl" width="200" height="100" />
@for (item of items; track item.id) {
<div>{{item}}</div>
}
Built-in a11y compliance for inclusive applications:
- Images:
alt-text
- Alternative text required - Keyboard Navigation:
click-events-have-key-events
,mouse-events-have-key-events
- Interactive Elements:
interactive-supports-focus
- Focusable interactive elements - Forms:
label-has-associated-control
- Proper form labeling - ARIA:
role-has-required-aria
,valid-aria
- Correct ARIA usage - Focus Management:
no-autofocus
,no-positive-tabindex
- Tables:
table-scope
- Proper scope attributes - Distracting Elements:
no-distracting-elements
- No marquee/blink
<!-- β Accessibility issues -->
<img [src]="imageUrl">
<div (click)="action()">Click me</div>
<input type="text">
<!-- β
Accessible patterns -->
<img [src]="imageUrl" [alt]="imageAlt">
<button (click)="action()">Click me</button>
<label for="name">Name</label>
<input id="name" type="text">
Style validation for component styles:
- Import Management:
no-duplicate-imports
- Prevents duplicate @import - Selectors:
no-duplicate-keyframe-selectors
- Unique keyframe names - At-Rules:
no-invalid-at-rules
,no-invalid-at-rule-placement
- Grid:
no-invalid-named-grid-areas
- Valid CSS Grid areas - Properties:
no-invalid-properties
- Catches typos in CSS properties - Quality:
no-empty-blocks
,font-family-fallbacks
- Best practices
// β CSS issues
@import 'theme';
@import 'theme'; // Duplicate
.empty {} // Empty block
.text {
font-family: 'CustomFont'; // No fallback
colr: red; // Typo
}
// β
Clean styles
@import 'theme';
.text {
font-family: 'CustomFont', sans-serif;
color: red;
}
Modern Angular communication patterns:
- Input Signals: Preferred over decorators for reactive inputs
- Output Events:
no-output-native
,no-output-on-prefix
- Avoids conflicts - Naming:
no-input-rename
,no-output-rename
- Consistent API - Metadata:
no-inputs-metadata-property
,no-outputs-metadata-property
- Readonly Outputs:
prefer-output-readonly
- Immutable event emitters
// β Old patterns
@Component({
inputs: ['value'],
outputs: ['onChange']
})
export class OldComponent {
@Input('aliased') value: string;
@Output() onClick = new EventEmitter();
}
// β
Modern patterns
@Component({...})
export class ModernComponent {
value = input<string>();
readonly changed = output<string>();
}
Relaxed rules for testing:
- Lifecycle calls allowed in tests
- OnPush change detection not required
- Injectable providedIn requirements relaxed
Flexible rules for component documentation:
- Console output allowed
- Indentation and line length unrestricted
- Component selector requirements relaxed
- Type safety warnings instead of errors
Practical rules for code generation:
- Console output allowed for CLI feedback
- Any types allowed for dynamic operations
- Unsafe operations permitted for metaprogramming
Migration support:
- Standalone preference as warning, not error
- Supports gradual migration to standalone
Special routing patterns:
- Component/directive selector rules disabled
- Supports Angular's file-based routing conventions
This package extends @noneforge/eslint-config which provides:
- Comprehensive TypeScript type checking
- Built-in formatting (Prettier replacement)
- Import organization and sorting
- Modern JavaScript best practices
- JSDoc documentation rules
See the base configuration README for details on inherited rules.
// β Traditional reactive patterns
export class OldComponent {
value = 0;
valueSubject = new BehaviorSubject(0);
ngOnInit() {
this.valueSubject.subscribe(v => this.value = v);
}
}
// β
Modern signals
export class ModernComponent {
value = signal(0);
computed = computed(() => this.value() * 2);
constructor() {
effect(() => {
console.log('Value changed:', this.value());
});
}
}
// β Constructor injection
export class OldService {
constructor(
private http: HttpClient,
private router: Router,
@Inject(CONFIG) private config: Config
) {}
}
// β
inject() function
export class ModernService {
private http = inject(HttpClient);
private router = inject(Router);
private config = inject(CONFIG);
}
// Template file
@if (user()) {
<app-user-profile [user]="user()" />
} @else if (loading()) {
<app-spinner />
} @else {
<p>No user found</p>
}
@for (item of items(); track item.id) {
<app-item [data]="item" />
} @empty {
<p>No items available</p>
}
@switch (status()) {
@case ('active') { <app-active /> }
@case ('pending') { <app-pending /> }
@default { <app-inactive /> }
}
Add to .vscode/settings.json
:
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.experimental.useFlatConfig": true,
"eslint.validate": [
"javascript",
"typescript",
"html",
"css",
"scss"
]
}
{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"lint:debug": "eslint . --debug",
"type-check": "tsc --noEmit"
}
}
- Remove
.eslintrc.*
files - Create
eslint.config.js
with flat config - Update VSCode settings for flat config
- Install this package and its peer dependencies
- Update scripts to use ESLint 9 CLI
- Use
projectService: true
for better TypeScript performance - Enable ESLint cache:
eslint . --cache
- Exclude
dist
and.angular
in your tsconfig.json - Consider
--max-warnings 0
in CI/CD pipelines
This configuration prioritizes:
- Modern Angular - Signals, standalone components, and latest APIs
- Accessibility - Built-in a11y rules for inclusive applications
- Performance - OnPush change detection and optimization patterns
- Type Safety - Leverage TypeScript for runtime error prevention
- Developer Experience - Clear errors with practical escape hatches
MIT
Issues and PRs welcome at GitHub