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 {
// Required input
name = input.required<string>();
// Optional input with default email = input<string>(''); showEmail = input(false);
// Input with transform isActive = input(false, { transform: booleanAttribute });
// Computed from inputs
avatarUrl = computed(() => https://api.example.com/avatar/${this.name()});
// Output selected = output<string>();
handleClick() { this.selected.emit(this.name()); } }
Signal Inputs
// Required - must be provided by parent name = input.required<string>();
// Optional with default value count = input(0);
// Optional without default (undefined allowed) label = input<string>();
// With alias for template binding size = input('medium', { alias: 'buttonSize' });
// With transform function disabled = input(false, { transform: booleanAttribute }); value = input(0, { transform: numberAttribute });
Signal Outputs
import { output, outputFromObservable } from '@angular/core';
// Basic output clicked = output<void>(); selected = output<Item>();
// With alias valueChange = output<number>({ alias: 'change' });
// From Observable (for RxJS interop) scroll$ = new Subject<number>(); scrolled = outputFromObservable(this.scroll$);
// Emit values 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: { // Static attributes 'role': 'button',
// Dynamic class bindings
'[class.primary]': 'variant() === "primary"',
'[class.disabled]': 'disabled()',
// Dynamic style bindings
'[style.--btn-color]': 'color()',
// Attribute bindings
'[attr.aria-disabled]': 'disabled()',
'[attr.tabindex]': 'disabled() ? -1 : 0',
// Event listeners
'(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 {}
// Usage: // <app-card> // <h2 card-header>Title</h2> // <p>Main content</p> // <button card-footer>Action</button> // </app-card>
Lifecycle Hooks
import { OnDestroy, OnInit, afterNextRender, afterRender } from '@angular/core';
export class My implements OnInit, OnDestroy { constructor() { // For DOM manipulation after render (SSR-safe) afterNextRender(() => { // Runs once after first render });
afterRender(() => {
// Runs after every render
});
}
ngOnInit() { /* Component initialized / } ngOnDestroy() { / Cleanup */ } }
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 .
<!-- Conditionals --> @if (isLoading()) { <app-spinner /> } @else if (error()) { <app-error [message]="error()" /> } @else { <app-content [data]="data()" /> }
<!-- Loops --> @for (item of items(); track item.id) { <app-item [item]="item" /> } @empty { <p>No items found</p> }
<!-- Switch --> @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:
<!-- Class bindings --> <div [class.active]="isActive()">Single class</div> <div [class]="classString()">Class string</div>
<!-- Style bindings --> <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.