ViewChild & ContentChild
Angular components often need a direct handle on something living inside them — a DOM element to focus, a child component instance to call a method on, or projected content to inspect. Angular provides two families of queries for this: view queries (@ViewChild / viewChild()) that reach into a component’s own template, and content queries (@ContentChild / contentChild()) that reach into content projected through <ng-content>. Modern Angular (17+) adds signal-based query functions that are reactive, type-safe, and far less error-prone than the decorator timing rules of the past.
View queries vs content queries
The distinction comes down to where the queried element lives relative to the component doing the querying.
| Query | What it finds | Decorator | Signal function |
|---|---|---|---|
| Single view child | An element/directive/component in this component’s template | @ViewChild | viewChild() |
| Multiple view children | All matches in this template | @ViewChildren | viewChildren() |
| Single content child | A single projected element passed via <ng-content> | @ContentChild | contentChild() |
| Multiple content children | All projected matches | @ContentChildren | contentChildren() |
View queries see what the component renders; content queries see what a parent projects into it. If your element sits between
<ng-content>tags supplied by a consumer, you need a content query.
Signal queries (the modern default)
Signal queries are the recommended approach in Angular 17.3+. They return a signal you read by calling it, integrate with effect() and computed(), and have predictable availability tied to the rendering lifecycle.
import { Component, ElementRef, viewChild, effect } from '@angular/core';
@Component({
selector: 'app-search-box',
standalone: true,
template: `
<input #queryInput type="search" placeholder="Search…" />
<button (click)="focusInput()">Focus</button>
`,
})
export class SearchBoxComponent {
// Reactive, read by calling the signal.
private queryInput = viewChild<ElementRef<HTMLInputElement>>('queryInput');
constructor() {
effect(() => {
const el = this.queryInput();
console.log('Input available:', !!el);
});
}
focusInput(): void {
this.queryInput()?.nativeElement.focus();
}
}
Output:
Input available: true
Required queries
When an element is guaranteed to exist, use viewChild.required() to drop the undefined from the type. Reading it before it resolves throws a clear runtime error instead of silently returning undefined.
import { Component, ElementRef, viewChild } from '@angular/core';
@Component({
selector: 'app-canvas',
standalone: true,
template: `<canvas #surface width="320" height="200"></canvas>`,
})
export class CanvasComponent {
// Type is Signal<ElementRef<HTMLCanvasElement>> — no undefined.
surface = viewChild.required<ElementRef<HTMLCanvasElement>>('surface');
draw(): void {
const ctx = this.surface().nativeElement.getContext('2d');
ctx?.fillRect(10, 10, 100, 60);
}
}
Querying by component or directive type
You can query by a template reference variable (a string) or by a component/directive class. Querying by class returns the instance, letting you call its public methods and read its signals.
import { Component, viewChild, contentChild } from '@angular/core';
import { TabComponent } from './tab.component';
@Component({
selector: 'app-panel',
standalone: true,
template: `<app-tab title="Overview" />`,
imports: [TabComponent],
})
export class PanelComponent {
firstTab = viewChild(TabComponent);
activate(): void {
this.firstTab()?.select(); // call a public method on the child instance
}
}
ContentChild and projected content
contentChild() queries elements the parent passes through projection. The host component does not render these itself — it receives them via <ng-content>.
import { Component, contentChild, contentChildren } from '@angular/core';
import { TabComponent } from './tab.component';
@Component({
selector: 'app-tab-group',
standalone: true,
template: `<div class="tabs"><ng-content /></div>`,
})
export class TabGroupComponent {
// First projected <app-tab>.
activeTab = contentChild(TabComponent);
// All projected tabs as a reactive signal of a readonly array.
allTabs = contentChildren(TabComponent);
count = () => this.allTabs().length;
}
<app-tab-group>
<app-tab title="Account" />
<app-tab title="Billing" />
</app-tab-group>
The classic decorator API
Decorator queries still work and remain common in existing codebases. Their key constraint is timing: view queries resolve in ngAfterViewInit, and content queries in ngAfterContentInit. Reading them earlier yields undefined.
import {
Component, ViewChild, ViewChildren, ElementRef, QueryList, AfterViewInit,
} from '@angular/core';
import { TabComponent } from './tab.component';
@Component({
selector: 'app-legacy',
standalone: true,
template: `
<input #name />
<app-tab title="One" />
<app-tab title="Two" />
`,
imports: [TabComponent],
})
export class LegacyComponent implements AfterViewInit {
@ViewChild('name') nameRef!: ElementRef<HTMLInputElement>;
@ViewChildren(TabComponent) tabs!: QueryList<TabComponent>;
ngAfterViewInit(): void {
this.nameRef.nativeElement.focus();
console.log('Tab count:', this.tabs.length);
}
}
Output:
Tab count: 2
The static option
@ViewChild accepts { static: true } for queries that resolve before change detection (an element not inside a @if/@for), making them available in ngOnInit. Signal queries make this flag unnecessary.
@ViewChild('header', { static: true }) header!: ElementRef;
Decorator vs signal queries
| Aspect | Decorator (@ViewChild) | Signal (viewChild()) |
|---|---|---|
| Result type | Plain property | Signal<T> (call to read) |
| Availability | Lifecycle hook only | Reactive, read any time after render |
| Reactivity | Manual | Works in effect/computed |
| Required variant | Not built in | viewChild.required() |
| Multiple results | QueryList<T> | Signal<readonly T[]> |
Best practices
- Prefer signal queries (
viewChild/contentChild) in new code — they are reactive, simpler, and eliminate lifecycle timing bugs. - Use
.required()when an element is guaranteed present so the type excludesundefinedand missing elements fail loudly. - Query by component/directive class to interact with instances; use template reference strings for plain DOM elements.
- Treat queried DOM elements as a last resort — prefer data binding and signals over direct
nativeElementmanipulation. - With decorator queries, read view children only in
ngAfterViewInitand content children only inngAfterContentInit. - Reach for
contentChild/contentChildrenonly for projected content; use view queries for elements your own template renders.