Write production-quality modern Angular (v17+) code day to day — standalone components with signal inputs/outputs, computed and effect done right, templates with @if/@for/@switch/@defer, typed reactive forms, HttpClient services with interceptors, RxJS-to-signal conversion, dependency injection with inject(), routing with lazy routes and input binding, directives and pipes, Angular Material/CDK usage, styling and accessibility.
When agents use itUse this whenever the user asks to build, implement, refactor or fix an Angular component, service, directive, pipe, form, dialog, table, or feature; asks "how do I do X in Angular"; pastes Angular code with a bug; or asks about signals vs RxJS, @Input vs input(), ngOnInit vs constructor, ViewChild, content projection, or template syntax. Also use it when generating any new Angular code, even small snippets.
Install
Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.
npx skills add AGCO-Global/org-skills --skill angular-development -a github-copilot
# or with the org installer (adds .github/skills/angular-development):
npx -y github:AGCO-Global/org-skills add skill angular-development
Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.
Installs the frontend-angular-skills plugin, which bundles all Frontend / Angular skills and keeps them updated.
/plugin marketplace add AGCO-Global/org-skills
/plugin install frontend-angular-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill angular-development -a claude-code
Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.
Installs into .agents/skills/, which Codex reads.
npx skills add AGCO-Global/org-skills --skill angular-development -a codex
Installs into Cursor's skills folder.
npx skills add AGCO-Global/org-skills --skill angular-development -a cursor
Installs into Gemini CLI's skills folder.
npx skills add AGCO-Global/org-skills --skill angular-development -a gemini-cli
Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.
Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.
Skill contents
Angular Development
Modern Angular code is small standalone components, signals for state, RxJS only at the edges, and templates that contain no logic beyond bindings. This skill is the implementation guide; architecture, performance and testing have their own skills. If the codebase is still on NgModules/@Input(), write new code in the modern style anyway — it coexists.
1. Before writing a component
Smart or presentational? Pages/containers inject stores and services; everything else takes input() and emits output().
What state does it own?signal() for owned state; computed() for everything derivable; nothing derived stored in a field.
Which UI states? loading / empty / error / success — each has a template branch.
Accessible role? Use Material/CDK or semantic HTML; never a div (click).
Standalone (default), OnPush always, host: {} for host bindings/classes/listeners.
input() / input.required() / output() / model() — never @Input()/@Output() in new code. transform: booleanAttribute | numberAttribute for attribute-style inputs.
computed() for derived values; protected readonly so templates can read them and tests can't poke them.
inject() in field initialisers; the constructor stays empty or trivial. No ngOnInit for things that can be field initialisers or computed.
Inline template for < ~30 lines, otherwise templateUrl. One component per file; selector prefix from the workspace (app-, or the lib prefix).
Component under ~200 lines; extract child components or a service beyond that.
viewChild() / contentChild() signal queries instead of decorators; viewChild.required when it must exist.
3. Signals and effects
Want to…
Use
Hold state
signal(initial); update with .set() / .update(fn) — never mutate the object inside
Derive
computed() — lazy, memoised, synchronous
Derive but allow local override (e.g. selected item resets when list changes)
linkedSignal()
Async data as state
toSignal(obs$, { initialValue }), resource() / rxResource() / httpResource() where available
Side effect on change (log, localStorage, imperative DOM/3rd-party API)
effect() — the only legitimate use
Signal → Observable (feed an RxJS pipeline)
toObservable(sig)
effect() rules: no signal writes inside (if you need one, you wanted computed/linkedSignal); untracked() for reads that shouldn't be dependencies; cleanup via the onCleanup callback; keep them rare and named by intent in a comment.
Arrays/objects in signals are immutable: items.update(list => [...list, x]), form.update(f => ({ ...f, name })).
4. Templates
Control flow: @if (x(); as value) {…} @else if {…} @else {…}, @for (item of items(); track item.id) {…} @empty {…}, @switch. track is mandatory and must be a stable id.
@defer for heavy, below-the-fold or interaction-gated components; always give @placeholder and @loading.
No function calls in bindings except signals/computeds and pure pipes. {{ total() }} fine; {{ calcTotal(items) }} not.
Bindings: [prop], (event), [(model)] two-way with model() inputs; [attr.aria-*] for ARIA; [class.active], [style.width.px].
Pipes for formatting (date, currency, number, custom pure pipes). Custom pipe = pure function with standalone: true.
Content projection: <ng-content select="[slot=actions]">; ng-template + ngTemplateOutlet for consumer-supplied templates.
Template reference variables #input + viewChild('input') for focus management.
Keep templates declarative: no business rules; if a condition needs three operators, make it a computed.
API class per resource returning typed Observables; no component touches HttpClient.
Response types from OpenAPI codegen; DTO → domain mapping in the API class if shapes differ.
Functional interceptors for auth, base URL, error normalisation, retry-with-backoff on idempotent GETs.
Consume in a feature store (signals or SignalStore) or toSignal/httpResource in the page; components get signals.
Cancel in-flight requests with switchMap on param changes; takeUntilDestroyed() on any manual subscription (there should be few).
Errors: catch in the store, map to a user message, expose error = signal<string | null>(null).
6. Forms
Typed Reactive Forms: new FormGroup({ email: new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.email] }) }) or inject(NonNullableFormBuilder).
Validation messages in the template via a small app-field-error component reading control.errors + touched; aria-invalid/aria-describedby wired.
form.getRawValue() typed on submit; disable submit while pending() signal is true; form.markAllAsTouched() on invalid submit.
Cross-field validators as ValidatorFn on the group; async validators only for server checks, debounced with updateOn: 'blur'.
Dynamic lists → FormArray; large forms split into child components that receive the sub-FormGroup as an input.required().
Watch Signal Forms as it stabilises; keep validation rules in one place regardless.
7. Routing (implementation)
provideRouter(routes, withComponentInputBinding(), withViewTransitions()); route params/query params arrive as input() signals on the routed component.
loadComponent/loadChildren for every feature route; route title set for a11y and analytics.
Functional CanActivateFn guards using inject(); return UrlTree for redirects.
Navigate with inject(Router).navigate([...]) or <a [routerLink]>; routerLinkActive for nav state.
Resolvers only for must-have-before-render data; otherwise skeleton + load in component.
8. Directives and pipes
Attribute directive for cross-cutting behaviour (appAutofocus, appHasPermission, appTooltip) with inject(ElementRef), input()s, host bindings, and DestroyRef for cleanup.
Structural behaviour via ngTemplateOutlet or createEmbeddedView — but prefer @if in templates; custom structural directives are rare now.
hostDirectives to compose behaviours onto components (e.g. CdkTrapFocus).
Pipes: pure, stateless, small; inject services only when necessary.
9. Material / CDK usage
Import each Material component into the standalone imports array (MatButtonModule, MatTableModule, …).
Theme via M3 tokens in styles.scss (mat.theme(...)); component-level colour via CSS custom properties, not ::ng-deep.
Dialogs: inject(MatDialog).open(EditOrderDialog, { data }) returning typed results; MatDialogRef in the dialog component; focus restored automatically.
CDK: Overlay for custom popovers, A11yModule (cdkTrapFocus, LiveAnnouncer), DragDrop, ScrollingModule for virtual lists, Clipboard, Layout (BreakpointObserver → toSignal).
10. Styling and accessibility
Component styles scoped by default; :host for root; CSS custom properties for theming; no ::ng-deep (use styling hooks or global tokens).
Layout with CSS grid/flex; spacing tokens; @media (prefers-reduced-motion) respected in animations (@angular/animations or CSS).
Semantic HTML + Material components give most a11y; add aria-* via [attr.], manage focus on dialogs/route change (LiveAnnouncer for async results), ensure @angular-eslint/template a11y rules pass.
i18n: i18n attributes / $localize or Transloco; no hardcoded user-facing strings in templates.
11. Definition of done for a component
Standalone, OnPush, signal inputs/outputs, track on every @for
No derived state in fields; no effect() writing signals; no subscribe() without teardown
Loading/empty/error/success branches rendered
Keyboard-operable, labelled, focus managed
Types strict, no any; strictTemplates passes
Spec covering main states (see angular-testing); story if in shared/ui
Anti-patterns to fix on sight
@Input()/@Output(), @ViewChild, constructor injection in new code.
effect(() => this.x.set(...)) — use computed/linkedSignal.
BehaviorSubject + .value as state; subscribe() in ngOnInit with no takeUntilDestroyed.
*ngIf/*ngFor, @for without track, track $index on reorderable data.
Function calls or new Date() in templates; impure pipes for formatting.
HttpClient inside components; business logic in components; any in templates.
::ng-deep, ViewEncapsulation.None to style Material internals.
Mutating a signal's object in place (this.items().push(x)).
setTimeout to fix ExpressionChangedAfterItHasBeenChecked.
---
name: angular-development
description: Write production-quality modern Angular (v17+) code day to day — standalone components with signal inputs/outputs, computed and effect done right, templates with @if/@for/@switch/@defer, typed reactive forms, HttpClient services with interceptors, RxJS-to-signal conversion, dependency injection with inject(), routing with lazy routes and input binding, directives and pipes, Angular Material/CDK usage, styling and accessibility. Use this whenever the user asks to build, implement, refactor or fix an Angular component, service, directive, pipe, form, dialog, table, or feature; asks "how do I do X in Angular"; pastes Angular code with a bug; or asks about signals vs RxJS, @Input vs input(), ngOnInit vs constructor, ViewChild, content projection, or template syntax. Also use it when generating any new Angular code, even small snippets.
metadata:
technology: Angular
type: development
---
# Angular Development
Modern Angular code is small standalone components, signals for state, RxJS only at the edges, and templates that contain no logic beyond bindings. This skill is the implementation guide; architecture, performance and testing have their own skills. If the codebase is still on NgModules/`@Input()`, write new code in the modern style anyway — it coexists.
## 1. Before writing a component
1. **Smart or presentational?** Pages/containers inject stores and services; everything else takes `input()` and emits `output()`.
2. **What state does it own?** `signal()` for owned state; `computed()` for everything derivable; nothing derived stored in a field.
3. **Which UI states?** loading / empty / error / success — each has a template branch.
4. **Accessible role?** Use Material/CDK or semantic HTML; never a `div (click)`.
## 2. Component template
```ts
import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
import { CurrencyPipe } from '@angular/common';
@Component({
selector: 'app-order-row',
imports: [CurrencyPipe],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'order-row', '[attr.aria-selected]': 'selected()' },
template: `
<button type="button" (click)="select.emit(order().id)" [attr.aria-label]="'Open order ' + order().number">
<span>{{ order().number }}</span>
<span>{{ order().total | currency: order().currency }}</span>
@if (isOverdue()) { <app-badge tone="warning">Overdue</app-badge> }
</button>
`,
})
export class OrderRowComponent {
readonly order = input.required<Order>();
readonly selected = input(false, { transform: booleanAttribute });
readonly select = output<Order['id']>();
protected readonly isOverdue = computed(() => this.order().dueDate < Date.now() && !this.order().paid);
}
```
Conventions:
- **Standalone** (default), **`OnPush`** always, `host: {}` for host bindings/classes/listeners.
- **`input()` / `input.required()` / `output()` / `model()`** — never `@Input()`/`@Output()` in new code. `transform: booleanAttribute | numberAttribute` for attribute-style inputs.
- **`computed()` for derived values**; `protected readonly` so templates can read them and tests can't poke them.
- **`inject()` in field initialisers**; the constructor stays empty or trivial. No `ngOnInit` for things that can be field initialisers or `computed`.
- **Inline template for < ~30 lines**, otherwise `templateUrl`. One component per file; selector prefix from the workspace (`app-`, or the lib prefix).
- **Component under ~200 lines**; extract child components or a service beyond that.
- **`viewChild()` / `contentChild()` signal queries** instead of decorators; `viewChild.required` when it must exist.
## 3. Signals and effects
| Want to… | Use |
|---|---|
| Hold state | `signal(initial)`; update with `.set()` / `.update(fn)` — never mutate the object inside |
| Derive | `computed()` — lazy, memoised, synchronous |
| Derive but allow local override (e.g. selected item resets when list changes) | `linkedSignal()` |
| Async data as state | `toSignal(obs$, { initialValue })`, `resource()` / `rxResource()` / `httpResource()` where available |
| Side effect on change (log, localStorage, imperative DOM/3rd-party API) | `effect()` — the *only* legitimate use |
| Signal → Observable (feed an RxJS pipeline) | `toObservable(sig)` |
`effect()` rules: no signal writes inside (if you need one, you wanted `computed`/`linkedSignal`); `untracked()` for reads that shouldn't be dependencies; cleanup via the `onCleanup` callback; keep them rare and named by intent in a comment.
Arrays/objects in signals are immutable: `items.update(list => [...list, x])`, `form.update(f => ({ ...f, name }))`.
## 4. Templates
- **Control flow**: `@if (x(); as value) {…} @else if {…} @else {…}`, `@for (item of items(); track item.id) {…} @empty {…}`, `@switch`. `track` is mandatory and must be a stable id.
- **`@defer`** for heavy, below-the-fold or interaction-gated components; always give `@placeholder` and `@loading`.
- **No function calls in bindings** except signals/computeds and pure pipes. `{{ total() }}` fine; `{{ calcTotal(items) }}` not.
- **Bindings**: `[prop]`, `(event)`, `[(model)]` two-way with `model()` inputs; `[attr.aria-*]` for ARIA; `[class.active]`, `[style.width.px]`.
- **Pipes** for formatting (`date`, `currency`, `number`, custom pure pipes). Custom pipe = pure function with `standalone: true`.
- **Content projection**: `<ng-content select="[slot=actions]">`; `ng-template` + `ngTemplateOutlet` for consumer-supplied templates.
- Template reference variables `#input` + `viewChild('input')` for focus management.
- Keep templates declarative: no business rules; if a condition needs three operators, make it a `computed`.
## 5. Services and HTTP
```ts
@Injectable({ providedIn: 'root' })
export class OrdersApi {
private readonly http = inject(HttpClient);
private readonly base = inject(API_BASE_URL);
list(filters: OrderFilters): Observable<Page<Order>> {
return this.http.get<Page<Order>>(`${this.base}/orders`, { params: toParams(filters) });
}
update(id: string, patch: Partial<Order>): Observable<Order> {
return this.http.patch<Order>(`${this.base}/orders/${id}`, patch);
}
}
```
- **API class per resource** returning typed Observables; no component touches `HttpClient`.
- Response types from OpenAPI codegen; DTO → domain mapping in the API class if shapes differ.
- **Functional interceptors** for auth, base URL, error normalisation, retry-with-backoff on idempotent GETs.
- Consume in a **feature store** (signals or SignalStore) or `toSignal`/`httpResource` in the page; components get signals.
- Cancel in-flight requests with `switchMap` on param changes; `takeUntilDestroyed()` on any manual subscription (there should be few).
- Errors: catch in the store, map to a user message, expose `error = signal<string | null>(null)`.
## 6. Forms
- **Typed Reactive Forms**: `new FormGroup({ email: new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.email] }) })` or `inject(NonNullableFormBuilder)`.
- Validation messages in the template via a small `app-field-error` component reading `control.errors` + `touched`; `aria-invalid`/`aria-describedby` wired.
- `form.getRawValue()` typed on submit; disable submit while `pending()` signal is true; `form.markAllAsTouched()` on invalid submit.
- Cross-field validators as `ValidatorFn` on the group; async validators only for server checks, debounced with `updateOn: 'blur'`.
- Dynamic lists → `FormArray`; large forms split into child components that receive the sub-`FormGroup` as an `input.required()`.
- Watch Signal Forms as it stabilises; keep validation rules in one place regardless.
## 7. Routing (implementation)
- `provideRouter(routes, withComponentInputBinding(), withViewTransitions())`; route params/query params arrive as `input()` signals on the routed component.
- `loadComponent`/`loadChildren` for every feature route; route `title` set for a11y and analytics.
- Functional `CanActivateFn` guards using `inject()`; return `UrlTree` for redirects.
- Navigate with `inject(Router).navigate([...])` or `<a [routerLink]>`; `routerLinkActive` for nav state.
- Resolvers only for must-have-before-render data; otherwise skeleton + load in component.
## 8. Directives and pipes
- Attribute directive for cross-cutting behaviour (`appAutofocus`, `appHasPermission`, `appTooltip`) with `inject(ElementRef)`, `input()`s, `host` bindings, and `DestroyRef` for cleanup.
- Structural behaviour via `ngTemplateOutlet` or `createEmbeddedView` — but prefer `@if` in templates; custom structural directives are rare now.
- `hostDirectives` to compose behaviours onto components (e.g. `CdkTrapFocus`).
- Pipes: pure, stateless, small; inject services only when necessary.
## 9. Material / CDK usage
- Import each Material component into the standalone `imports` array (`MatButtonModule`, `MatTableModule`, …).
- Theme via M3 tokens in `styles.scss` (`mat.theme(...)`); component-level colour via CSS custom properties, not `::ng-deep`.
- Dialogs: `inject(MatDialog).open(EditOrderDialog, { data })` returning typed results; `MatDialogRef` in the dialog component; focus restored automatically.
- CDK: `Overlay` for custom popovers, `A11yModule` (`cdkTrapFocus`, `LiveAnnouncer`), `DragDrop`, `ScrollingModule` for virtual lists, `Clipboard`, `Layout` (`BreakpointObserver` → `toSignal`).
## 10. Styling and accessibility
- Component styles scoped by default; `:host` for root; CSS custom properties for theming; no `::ng-deep` (use styling hooks or global tokens).
- Layout with CSS grid/flex; spacing tokens; `@media (prefers-reduced-motion)` respected in animations (`@angular/animations` or CSS).
- Semantic HTML + Material components give most a11y; add `aria-*` via `[attr.]`, manage focus on dialogs/route change (`LiveAnnouncer` for async results), ensure `@angular-eslint/template` a11y rules pass.
- i18n: `i18n` attributes / `$localize` or Transloco; no hardcoded user-facing strings in templates.
## 11. Definition of done for a component
- [ ] Standalone, OnPush, signal inputs/outputs, `track` on every `@for`
- [ ] No derived state in fields; no `effect()` writing signals; no `subscribe()` without teardown
- [ ] Loading/empty/error/success branches rendered
- [ ] Keyboard-operable, labelled, focus managed
- [ ] Types strict, no `any`; `strictTemplates` passes
- [ ] Spec covering main states (see `angular-testing`); story if in `shared/ui`
## Anti-patterns to fix on sight
- `@Input()`/`@Output()`, `@ViewChild`, constructor injection in new code.
- `effect(() => this.x.set(...))` — use `computed`/`linkedSignal`.
- `BehaviorSubject` + `.value` as state; `subscribe()` in `ngOnInit` with no `takeUntilDestroyed`.
- `*ngIf`/`*ngFor`, `@for` without `track`, `track $index` on reorderable data.
- Function calls or `new Date()` in templates; impure pipes for formatting.
- `HttpClient` inside components; business logic in components; `any` in templates.
- `::ng-deep`, `ViewEncapsulation.None` to style Material internals.
- Mutating a signal's object in place (`this.items().push(x)`).
- `setTimeout` to fix `ExpressionChangedAfterItHasBeenChecked`.