| name | angular-component |
| description | Create modern Angular standalone components following v20+ best practices. Use for building UI components with signal-based inputs/outputs, OnPush change detection, host bindings, content projection, and lifecycle hooks. Triggers on component creation, refactoring class-based inputs to signals, adding host bindings, or implementing accessible interactive components. |
Angular Component
Create standalone components for Angular v20+. Components are standalone by default—do NOT set standalone: true.
Component Structure
import {
Component,
ChangeDetectionStrategy,
input,
output,
computed,
} from "@angular/core";
@Component({
selector: "app-user-card",
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
class: "user-card",
"[class.active]": "isActive()",
"(click)": "handleClick()",
},
template: `
<img [src]="avatarUrl()" [alt]="name() + ' avatar'" />
<h2>{{ name() }}</h2>
@if (showEmail()) {
<p>{{ email() }}</p>
}
`,
styles: `
:host {
display: block;
}
:host.active {
border: 2px solid blue;
}
`,
})
export class UserCard {
name = input.required<string>();
email = input<string>("");
showEmail = input(false);
isActive = input(false, { transform: booleanAttribute });
avatarUrl = computed(() => `https://api.example.com/avatar/${this.name()}`);
selected = output<string>();
handleClick() {
this.selected.emit(this.name());
}
}
Signal Inputs
name = input.required<string>();
count = input(0);
label = input<string>();
size = input("medium", { alias: "buttonSize" });
disabled = input(false, { transform: booleanAttribute });
value = input(0, { transform: numberAttribute });
Signal Outputs
import { output, outputFromObservable } from "@angular/core";
clicked = output<void>();
selected = output<Item>();
valueChange = output<number>({ alias: "change" });
scroll$ = new Subject<number>();
scrolled = outputFromObservable(this.scroll$);
this.clicked.emit();
this.selected.emit(item);
Host Bindings
Use the host object in @Component—do NOT use @HostBinding or @HostListener decorators.
@Component({
selector: "app-button",
host: {
role: "button",
"[class.primary]": 'variant() === "primary"',
"[class.disabled]": "disabled()",
"[style.--btn-color]": "color()",
"[attr.aria-disabled]": "disabled()",
"[attr.tabindex]": "disabled() ? -1 : 0",
"(click)": "onClick($event)",
"(keydown.enter)": "onClick($event)",
"(keydown.space)": "onClick($event)",
},
template: `<ng-content />`,
})
export class Button {
variant = input<"primary" | "secondary">("primary");
disabled = input(false, { transform: booleanAttribute });
color = input("#007bff");
clicked = output<void>();
onClick(event: Event) {
if (!this.disabled()) {
this.clicked.emit();
}
}
}
Content Projection
@Component({
selector: "app-card",
template: `
<header>
<ng-content select="[card-header]" />
</header>
<main>
<ng-content />
</main>
<footer>
<ng-content select="[card-footer]" />
</footer>
`,
})
export class Card {}
Lifecycle Hooks
import { OnDestroy, OnInit, afterNextRender, afterRender } from "@angular/core";
export class My implements OnInit, OnDestroy {
constructor() {
afterNextRender(() => {
});
afterRender(() => {
});
}
ngOnInit() {
}
ngOnDestroy() {
}
}
Accessibility Requirements
Components MUST:
- Pass AXE accessibility checks
- Meet WCAG AA standards
- Include proper ARIA attributes for interactive elements
- Support keyboard navigation
- Maintain visible focus indicators
@Component({
selector: "app-toggle",
host: {
role: "switch",
"[attr.aria-checked]": "checked()",
"[attr.aria-label]": "label()",
tabindex: "0",
"(click)": "toggle()",
"(keydown.enter)": "toggle()",
"(keydown.space)": "toggle(); $event.preventDefault()",
},
template: `<span class="toggle-track"
><span class="toggle-thumb"></span
></span>`,
})
export class Toggle {
label = input.required<string>();
checked = input(false, { transform: booleanAttribute });
checkedChange = output<boolean>();
toggle() {
this.checkedChange.emit(!this.checked());
}
}
Template Syntax
Use native control flow—do NOT use *ngIf, *ngFor, *ngSwitch.
@if (isLoading()) {
<app-spinner />
} @else if (error()) {
<app-error [message]="error()" />
} @else {
<app-content [data]="data()" />
}
@for (item of items(); track item.id) {
<app-item [item]="item" />
} @empty {
<p>No items found</p>
}
@switch (status()) { @case ('pending') { <span>Pending</span> } @case ('active')
{ <span>Active</span> } @default { <span>Unknown</span> } }
Class and Style Bindings
Do NOT use ngClass or ngStyle. Use direct bindings:
<div [class.active]="isActive()">Single class</div>
<div [class]="classString()">Class string</div>
<div [style.color]="textColor()">Styled text</div>
<div [style.width.px]="width()">With unit</div>
Images
Use NgOptimizedImage for static images:
import { NgOptimizedImage } from "@angular/common";
@Component({
imports: [NgOptimizedImage],
template: `
<img ngSrc="/assets/hero.jpg" width="800" height="600" priority />
<img [ngSrc]="imageUrl()" width="200" height="200" />
`,
})
export class Hero {
imageUrl = input.required<string>();
}
For detailed patterns, see references/component-patterns.md.