Find child elements, components and projected content as signals with viewChild, viewChildren, contentChild and contentChildren.

Bishal Biswas
Self Learner
Reaching into the template
A query finds something rendered by your component and gives you a handle to it.
import { Component, viewChild, ElementRef } from '@angular/core';
@Component({
selector: 'app-search',
template: `
<input #box>
<button (click)="focusInput()">Focus</button>
`,
})
export class Search {
private readonly box = viewChild<ElementRef<HTMLInputElement>>('box');
protected focusInput(): void {
this.box()?.nativeElement.focus();
}
}
viewChild returns a signal. Reading it with brackets gives the current result, or undefined before the view exists.
Required queries
private readonly box = viewChild.required<ElementRef<HTMLInputElement>>('box');
protected focusInput(): void {
this.box().nativeElement.focus(); // no ?. needed
}
required removes undefined from the type. It throws if the element is genuinely missing, which is what you want for something that is always in the template.
Use required for elements outside any @if. Use the plain version for anything conditional.
The four queries
viewChild one thing in this component's own template
viewChildren all matching things in this component's template
contentChild one thing projected in via ng-content
contentChildren all matching projected things
View means markup this component owns. Content means markup the caller passed in.
@Component({
selector: 'app-tabs',
template: `
<nav #tabBar></nav> <!-- view: mine -->
<ng-content /> <!-- content: the caller's -->
`,
})
export class Tabs {
readonly bar = viewChild.required<ElementRef>('tabBar');
readonly panels = contentChildren(TabPanel);
}
What you can query for
// a template reference variable
readonly box = viewChild<ElementRef>('box');
// a component type
readonly child = viewChild(UserCard);
// a directive type
readonly tooltip = viewChild(TooltipDirective);
// a TemplateRef
readonly row = contentChild(TemplateRef);
// with an explicit read
readonly element = viewChild('box', { read: ElementRef });
readonly container = viewChild('slot', { read: ViewContainerRef });
The read option matters when one element could give you several things. A component tag can yield the component instance, its ElementRef, or its ViewContainerRef.
readonly instance = viewChild(UserCard); // the component
readonly element = viewChild(UserCard, { read: ElementRef }); // its DOM element
Queries are reactive
This is the improvement over the decorator versions. A signal query updates automatically as the DOM changes.
@Component({
template: `
@for (item of items(); track item.id) {
<app-row [item]="item" />
}
<p>{{ rows().length }} rows rendered</p>
`,
})
export class List {
readonly items = signal<Item[]>([]);
protected readonly rows = viewChildren(Row);
}
Add an item and rows() grows. No lifecycle hook, no QueryList subscription.
viewChildren returns a Signal<readonly T[]>, so it composes with computed:
protected readonly rowCount = computed(() => this.rows().length);
protected readonly anySelected = computed(() => this.rows().some(r => r.selected()));
Descendants
contentChildren only looks at direct children by default. Pass descendants to search deeper.
readonly items = contentChildren(MenuItem); // direct children
readonly allItems = contentChildren(MenuItem, { descendants: true }); // nested too
viewChildren always searches the whole view.
When results are available
Queries resolve after the view is created. Reading one in a constructor gives undefined.
export class Search {
private readonly box = viewChild<ElementRef>('box');
constructor() {
console.log(this.box()); // undefined
}
ngAfterViewInit(): void {
console.log(this.box()); // the element
}
}
Better than a lifecycle hook, use an effect. It runs when the query resolves and again if it changes.
constructor() {
effect(() => {
const box = this.box();
if (box) box.nativeElement.focus();
});
}
For anything measuring layout, use afterRenderEffect instead.
A worked example
@Component({
selector: 'app-accordion',
template: `<ng-content />`,
})
export class Accordion {
readonly panels = contentChildren(AccordionPanel);
constructor() {
effect(() => {
const panels = this.panels();
// close every panel except the open one
const open = panels.find(p => p.expanded());
untracked(() => {
for (const panel of panels) {
if (panel !== open) panel.expanded.set(false);
}
});
});
}
}
<app-accordion>
<app-accordion-panel title="One">...</app-accordion-panel>
<app-accordion-panel title="Two">...</app-accordion-panel>
</app-accordion>
The accordion coordinates panels it did not create. This is the pattern behind tabs, steppers, radio groups and menus.
What changed from the decorators
// old
@ViewChild('box') box!: ElementRef;
@ViewChild('box', { static: true }) staticBox!: ElementRef;
@ViewChildren(Row) rows!: QueryList<Row>;
@ContentChildren(Tab, { descendants: true }) tabs!: QueryList<Tab>;
ngAfterViewInit(): void {
this.rows.changes.subscribe(() => this.recount());
}
// new
readonly box = viewChild.required<ElementRef>('box');
readonly rows = viewChildren(Row);
readonly tabs = contentChildren(Tab, { descendants: true });
protected readonly count = computed(() => this.rows().length);
Three real gains:
No static option. That flag existed because results appeared at different times depending on whether the element was inside an *ngIf. Signals removed the problem.
No QueryList. A custom collection with its own changes observable is replaced by a plain array in a signal.
No lifecycle hooks. computed and effect react to query changes directly.
Migrate with:
ng generate @angular/core:signal-queries-migration
Common mistakes
Reading a query in a field initialiser.
readonly box = viewChild<ElementRef>('box');
readonly width = this.box()?.nativeElement.offsetWidth; // undefined, too early
Querying inside @if with required. It throws when the condition is false. Use the optional form.
Mutating the DOM found by a query. Angular owns that DOM. Prefer bindings, and reach for direct manipulation only for focus, scroll and measurement.
Quick recap
viewChildandviewChildrenfind things in your own template.contentChildandcontentChildrenfind things projected in by the caller.- All four return signals, so they compose with
computedandeffect. .requiredremovesundefinedbut throws if the target is missing.- Use
readwhen one element could yield several different things. contentChildrenneedsdescendants: trueto look past direct children.- Signal queries removed the
staticflag,QueryListand the lifecycle hooks.
import { Component, ElementRef, TemplateRef, ViewContainerRef, signal,
computed, effect, untracked, viewChild, viewChildren,
contentChild, contentChildren } from '@angular/core';
@Component({
selector: 'app-queries',
template: `
<input #box>
@for (item of items(); track item.id) { <app-row [item]="item" /> }
<ng-content />
`,
})
export class Queries {
readonly items = signal<{ id: number }[]>([]);
// VIEW = markup this component owns
private readonly box = viewChild.required<ElementRef<HTMLInputElement>>('box');
protected readonly rows = viewChildren(Row);
// CONTENT = markup the caller projected in
readonly panels = contentChildren(Panel, { descendants: true });
readonly rowTemplate = contentChild(TemplateRef);
// `read` picks WHICH thing you get from one element
private readonly instance = viewChild(Row);
private readonly element = viewChild(Row, { read: ElementRef });
private readonly slot = viewChild('box', { read: ViewContainerRef });
// Queries are SIGNALS, so they compose. No QueryList, no changes subscription.
protected readonly rowCount = computed(() => this.rows().length);
constructor() {
// Runs when the query resolves, and again whenever it changes.
// Better than ngAfterViewInit.
effect(() => this.box().nativeElement.focus());
// Coordinating projected children: the accordion pattern
effect(() => {
const panels = this.panels();
const open = panels.find(p => p.expanded());
untracked(() => {
for (const p of panels) if (p !== open) p.expanded.set(false);
});
});
}
}
@Component({ selector: 'app-row', template: `` })
export class Row {}
@Component({ selector: 'app-panel', template: `` })
export class Panel { readonly expanded = signal(false); }
// What went away compared with @ViewChild:
// { static: true } no longer needed, signals removed the timing problem
// QueryList<T> now a plain readonly array in a signal
// ngAfterViewInit use effect() or computed() instead
// Migrate with: ng generate @angular/core:signal-queries-migration
// Reading a query in a FIELD INITIALISER is too early and gives undefined.
Was this explanation helpful?
Your feedback helps us improve the content for everyone.
Thank you for your feedback!
We appreciate you helping us improve.
What could we improve? (Optional)
Read aloud
Ready