---
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
```typescript
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: `
{{ name() }}
@if (showEmail()) {
{{ email() }}
}
`,
styles: `
:host { display: block; }
:host.active { border: 2px solid blue; }
`,
})
export class UserCardComponent {
// Required input
name = input.required();
// Optional input with default
email = input('');
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();
handleClick() {
this.selected.emit(this.name());
}
}
```
## Signal Inputs
```typescript
// Required - must be provided by parent
name = input.required();
// Optional with default value
count = input(0);
// Optional without default (undefined allowed)
label = input();
// 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
```typescript
import { output, outputFromObservable } from '@angular/core';
// Basic output
clicked = output();
selected = output();
// With alias
valueChange = output({ alias: 'change' });
// From Observable (for RxJS interop)
scroll$ = new Subject();
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.
```typescript
@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: ``,
})
export class ButtonComponent {
variant = input<'primary' | 'secondary'>('primary');
disabled = input(false, { transform: booleanAttribute });
color = input('#007bff');
clicked = output();
onClick(event: Event) {
if (!this.disabled()) {
this.clicked.emit();
}
}
}
```
## Content Projection
```typescript
@Component({
selector: 'app-card',
template: `
`,
})
export class CardComponent {}
// Usage:
//
//
Title
//
Main content
//
//
```
## Lifecycle Hooks
```typescript
import {
AfterContentInit, AfterViewInit, OnDestroy, OnInit,
afterNextRender, afterRender
} from '@angular/core';
export class MyComponent implements OnInit, AfterContentInit, AfterViewInit, OnDestroy {
constructor() {
// For DOM manipulation after render (SSR-safe)
afterNextRender(() => {
// Runs once after first render
});
afterRender(() => {
// Runs after every render
});
}
ngOnInit() { /* Component initialized */ }
ngAfterContentInit() { /* Projected content ready */ }
ngAfterViewInit() { /* View children ready */ }
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
```typescript
@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: ``,
})
export class ToggleComponent {
label = input.required();
checked = input(false, { transform: booleanAttribute });
checkedChange = output();
toggle() {
this.checkedChange.emit(!this.checked());
}
}
```
## Template Syntax
Use native control flow—do NOT use `*ngIf`, `*ngFor`, `*ngSwitch`.
```html
@if (isLoading()) {
} @else if (error()) {
} @else {
}
@for (item of items(); track item.id) {
} @empty {
No items found
}
@switch (status()) {
@case ('pending') { Pending }
@case ('active') { Active }
@default { Unknown }
}
```
## Class and Style Bindings
Do NOT use `ngClass` or `ngStyle`. Use direct bindings:
```html
Single class
Class string
Styled text
With unit
```
## Images
Use `NgOptimizedImage` for static images:
```typescript
import { NgOptimizedImage } from '@angular/common';
@Component({
imports: [NgOptimizedImage],
template: `
`,
})
export class HeroComponent {
imageUrl = input.required();
}
```
For detailed patterns, see [references/component-patterns.md](references/component-patterns.md).