---
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 UserCard {
// 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 Button {
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 Card {}
// Usage:
//
//
Title
//
Main content
//
//
```
## Lifecycle Hooks
```typescript
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
```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 Toggle {
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 Hero {
imageUrl = input.required();
}
```
For detailed patterns, see [references/component-patterns.md](references/component-patterns.md).