ViewChild & ContentChild in Angular
Angular provides four decorators for accessing elements, directives, or child components from a parent:
@ViewChild/@ViewChildren— access elements in the component's own template@ContentChild/@ContentChildren— access elements projected into the component via<ng-content>
Understanding when each is available in the lifecycle is critical to using them correctly.
Quick Reference
Decorator | Targets | Available From |
|---|---|---|
@ViewChild | Component's own template (child components, DOM elements, directives) | ngAfterViewInit |
@ViewChildren | QueryList of all matching elements in own template | ngAfterViewInit |
@ContentChild | First projected content element (passed via ng-content) | ngAfterContentInit |
@ContentChildren | QueryList of all projected content elements | ngAfterContentInit |
@ViewChild — Access a Child Component
// src/app/child/child.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-child',
standalone: true,
template: `<p>Child: {{ message }}</p>`,
})
export class ChildComponent {
message = 'Hello from child!';
greet(name: string): void {
this.message = `Hello, ${name}!`;
}
}
// src/app/parent/parent.component.ts
import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { ChildComponent } from '../child/child.component';
@Component({
selector: 'app-parent',
standalone: true,
imports: [ChildComponent],
template: `
<app-child />
<button (click)="greetChild()">Greet Child</button>
`,
})
export class ParentComponent implements AfterViewInit {
@ViewChild(ChildComponent) child!: ChildComponent;
ngAfterViewInit(): void {
// Safe to access child here
console.log(child.message); // 'Hello from child!'
}
greetChild(): void {
this.child.greet('Angular');
}
}@ViewChild in ngOnInit — the view hasn't been initialized yet. Use ngAfterViewInit or the static: true option for static references.@ViewChild — Access a DOM Element
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-input-focus',
standalone: true,
template: `
<input #myInput type="text" placeholder="I'll auto-focus" />
<button (click)="focusInput()">Focus</button>
`,
})
export class InputFocusComponent implements AfterViewInit {
@ViewChild('myInput') inputRef!: ElementRef<HTMLInputElement>;
ngAfterViewInit(): void {
this.inputRef.nativeElement.focus();
}
focusInput(): void {
this.inputRef.nativeElement.focus();
this.inputRef.nativeElement.select();
}
}#myInput) to target specific DOM elements or directives with @ViewChild.static: true — For Elements Always in the Template
By default, @ViewChild is resolved after change detection. Set static: true when the element is never inside an @if or *ngIf — this makes it available in ngOnInit.
@Component({
template: `<canvas #chart></canvas>`, // always present, not behind *ngIf
})
export class ChartComponent implements OnInit {
// static: true — available in ngOnInit
@ViewChild('chart', { static: true }) canvas!: ElementRef<HTMLCanvasElement>;
ngOnInit(): void {
// Safe to use because static: true
const ctx = this.canvas.nativeElement.getContext('2d');
// initialize chart...
}
}@ViewChild with read — Access a Directive or ViewContainerRef
import { Component, ViewChild, ViewContainerRef } from '@angular/core';
@Component({
template: `<ng-template #host></ng-template>`,
})
export class DynamicHostComponent {
// Get the ViewContainerRef of the ng-template, not the TemplateRef itself
@ViewChild('host', { read: ViewContainerRef })
host!: ViewContainerRef;
loadComponent(): void {
const ref = this.host.createComponent(SomeDynamicComponent);
ref.setInput('data', 'hello');
}
}@ViewChildren — QueryList of Multiple Elements
import { Component, ViewChildren, QueryList, AfterViewInit, ElementRef } from '@angular/core';
@Component({
selector: 'app-items',
standalone: true,
template: `
@for (item of items; track item) {
<div #itemEl class="item">{{ item }}</div>
}
`,
})
export class ItemsComponent implements AfterViewInit {
items = ['Apple', 'Banana', 'Cherry'];
@ViewChildren('itemEl') itemElements!: QueryList<ElementRef<HTMLDivElement>>;
ngAfterViewInit(): void {
// Initial list
console.log('Item count:', this.itemElements.length);
// QueryList is live — fires on add/remove
this.itemElements.changes.subscribe((list: QueryList<ElementRef>) => {
console.log('Items changed, new count:', list.length);
});
}
highlight(index: number): void {
const el = this.itemElements.get(index);
if (el) {
el.nativeElement.style.background = 'yellow';
}
}
}QueryList is a live collection — it automatically updates when items are added or removed from the DOM. Subscribe to changes to react to those updates.@ContentChild — Projected Content
@ContentChild lets a parent component access content that consumers project into it via <ng-content>.
// src/app/components/card/card.component.ts
import { Component, ContentChild, AfterContentInit } from '@angular/core';
import { CardHeaderComponent } from './card-header.component';
import { CardFooterComponent } from './card-footer.component';
@Component({
selector: 'app-card',
standalone: true,
template: `
<div class="card">
<div class="card-header">
<ng-content select="app-card-header" />
</div>
<div class="card-body">
<ng-content />
</div>
<div class="card-footer" *ngIf="hasFooter">
<ng-content select="app-card-footer" />
</div>
</div>
`,
})
export class CardComponent implements AfterContentInit {
@ContentChild(CardHeaderComponent) header?: CardHeaderComponent;
@ContentChild(CardFooterComponent) footer?: CardFooterComponent;
hasFooter = false;
ngAfterContentInit(): void {
// Now we can access projected components
this.hasFooter = !!this.footer;
if (this.header) {
console.log('Card has header:', this.header.title);
}
}
}
// Usage:
// <app-card>
// <app-card-header title="My Card" />
// <p>Card body content</p>
// <app-card-footer>Save | Cancel</app-card-footer>
// </app-card>@ContentChildren — All Projected Items
A common pattern is a tab group that reads all projected tab components.
// src/app/components/tabs/tab.component.ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-tab',
standalone: true,
template: `
@if (active) {
<div class="tab-panel"><ng-content /></div>
}
`,
})
export class TabComponent {
@Input() label = '';
active = false;
}
// src/app/components/tabs/tab-group.component.ts
import {
Component, ContentChildren, QueryList, AfterContentInit
} from '@angular/core';
import { TabComponent } from './tab.component';
import { NgFor } from '@angular/common';
@Component({
selector: 'app-tab-group',
standalone: true,
imports: [NgFor],
template: `
<div class="tab-bar">
<button
*ngFor="let tab of tabs"
[class.active]="tab.active"
(click)="select(tab)"
>{{ tab.label }}</button>
</div>
<ng-content />
`,
})
export class TabGroupComponent implements AfterContentInit {
@ContentChildren(TabComponent) tabs!: QueryList<TabComponent>;
ngAfterContentInit(): void {
// Activate the first tab
const first = this.tabs.first;
if (first) first.active = true;
}
select(selected: TabComponent): void {
this.tabs.forEach(tab => (tab.active = false));
selected.active = true;
}
}
// Usage:
// <app-tab-group>
// <app-tab label="Overview">Overview content</app-tab>
// <app-tab label="Details">Details content</app-tab>
// <app-tab label="Reviews">Reviews content</app-tab>
// </app-tab-group>Signal-Based Queries (Angular 17+)
Angular 17 introduced signal-based query functions as an alternative to the decorator-based approach. These return signals instead of properties, integrating seamlessly with the signal ecosystem.
import { Component, viewChild, contentChild, contentChildren, ElementRef } from '@angular/core';
@Component({
selector: 'app-signal-queries',
standalone: true,
template: `
<input #myInput />
<ng-content />
`,
})
export class SignalQueriesComponent {
// viewChild<T>(locator) — returns Signal<T | undefined>
input = viewChild<ElementRef>('myInput');
// contentChild<T>(Type) — returns Signal<T | undefined>
header = contentChild(HeaderComponent);
// contentChildren<T>(Type) — returns Signal<readonly T[]>
tabs = contentChildren(TabComponent);
constructor() {
// Use in effect() or computed() — no lifecycle hook needed
effect(() => {
const inputEl = this.input();
if (inputEl) {
inputEl.nativeElement.focus();
}
});
}
}viewChild(), contentChildren()) can be used in constructor or effect() without waiting for ngAfterViewInit — the signal simply returns undefined before initialization.Comparison: Decorator vs Signal Queries
Decorator (@ViewChild) | Signal (viewChild()) | |
|---|---|---|
Syntax | @ViewChild(ref) child!: T | child = viewChild<T>(ref) |
Access | this.child (after view init) | this.child() (signal call) |
Lifecycle | Must wait for ngAfterViewInit | Signal is undefined until ready |
Reactivity | Manual change detection | Automatic with computed/effect |
Angular version | All versions | Angular 17+ |
Best Practices
Access @ViewChild values in ngAfterViewInit, not ngOnInit
Use static: true only for elements that are never inside *ngIf/@if
Use { read: ViewContainerRef } to get a ViewContainerRef from a template ref
Subscribe to QueryList.changes to react to dynamic additions/removals
Prefer @ContentChild over @Input for structural child components (tabs, accordions)
In Angular 17+, prefer signal-based viewChild/contentChild for new code
Never manipulate DOM via ElementRef.nativeElement for data binding — use Angular bindings
Summary
@ViewChild, @ViewChildren, @ContentChild, and @ContentChildren give Angular components precise access to child components, directives, and DOM elements. The key distinction is the lifecycle hook — view queries are ready in ngAfterViewInit and content queries in ngAfterContentInit. Angular 17+ signal-based equivalents (viewChild(), contentChildren()) integrate natively with the signal reactivity system and are the recommended approach for new code.