Angular Signal Queries: viewChild, contentChild, viewChildren, contentChildren [2026]
When a component needs a reference to one of its own template elements, a child component, or content projected into it, the answer is a query. Since Angular 17.2, queries are signals — viewChild(), contentChild(), and their plural cousins. The signal-based versions replaced the decorator-based @ViewChild/@ContentChild from the previous era and are now the standard.
This is lesson 2.13 of the Angular Tutorial. It assumes you understand content projection (lesson 2.6) and template reference variables (lesson 2.2).
The four query functions #
Four functions cover every case:
| Function | Returns | What it queries |
|---|---|---|
viewChild<T>() |
Signal<T | undefined> |
One element/component in this component's own template |
viewChildren<T>() |
Signal<readonly T[]> |
Many elements/components in this component's own template |
contentChild<T>() |
Signal<T | undefined> |
One element/component projected into this component (via <ng-content>) |
contentChildren<T>() |
Signal<readonly T[]> |
Many projected elements/components |
The singular returns one or undefined; the plural returns an array. All four are signals — read them with (), react to them in computed() or effect().
Querying by template-reference variable #
The most common case — reference an element you marked with #name:
import { Component, viewChild, ElementRef, afterNextRender } from '@angular/core';
@Component({
selector: 'app-search-box',
template: `
<input #search type="search" />
<button (click)="focusInput()">Focus</button>
`,
})
export class SearchBoxComponent {
searchInput = viewChild<ElementRef<HTMLInputElement>>('search');
focusInput() {
this.searchInput()?.nativeElement.focus();
}
}
Three facts:
'search'is the template reference name. Matches#searchin the template.- The generic
<ElementRef<HTMLInputElement>>types the return. Without it the signal is typed loosely. - Read with
searchInput()and check forundefined. The query returns undefined until the template renders.
The singular pattern is universal — declare with a string, type with a generic, read with () + null-check.
Querying by class #
For child components and directives, you can query by class instead of by template name:
import { Component, viewChild, viewChildren } from '@angular/core';
import { TabComponent } from './tab.component';
@Component({
selector: 'app-tabs',
imports: [TabComponent],
template: `
<app-tab #firstTab label="General" />
<app-tab label="Privacy" />
<app-tab label="Account" />
`,
})
export class TabsComponent {
firstTab = viewChild('firstTab'); // by reference variable
firstTabByClass = viewChild(TabComponent); // by class — same component
allTabs = viewChildren(TabComponent); // all instances
}
Querying by class is the right approach when:
- You have multiple instances of the same component
- You want them all (use the plural)
- You don't want to add
#nameto each one
The singular viewChild(TabComponent) returns the FIRST instance. The plural viewChildren(TabComponent) returns all of them, in template order.
required: true — query that must succeed #
A query that must always resolve can be marked required:
export class FormComponent {
// Throws an error at runtime if the template doesn't have a #form element
formEl = viewChild.required<ElementRef<HTMLFormElement>>('form');
submit() {
this.formEl().nativeElement.submit(); // no null-check needed — typed as ElementRef, not undefined
}
}
The signal is typed as T, not T | undefined. Use this when the absence of the queried element is a programmer error, not a runtime possibility.
Reading by type from a directive — the read option #
When the same template element has multiple meaningful types (a component AND a directive AND an ElementRef), the read option picks which:
@Component({
template: `<app-card appHoverGlow #ref>...</app-card>`,
})
export class HostComponent {
// Default — gets the component instance
card = viewChild(CardComponent);
// Explicitly the directive instance
glow = viewChild(CardComponent, { read: HoverGlowDirective });
// Explicitly the host element ref
el = viewChild('ref', { read: ElementRef });
// Explicitly the template ref (for `<ng-template>`)
tpl = viewChild('ref', { read: TemplateRef });
}
The read option disambiguates when there is more than one viable token. The default is the component class for class-queries, and ElementRef for template-name queries on regular elements.
When the query is populated #
This is the timing rule that catches everyone:
viewChildis populated after the view has renderedcontentChildis populated after content is projected
Reading the signal in the constructor returns undefined — the template hasn't rendered yet. Read it inside an effect() (reacts when populated), inside afterNextRender() (runs after first render), or in any event handler / template binding.
export class SearchBoxComponent {
searchInput = viewChild<ElementRef<HTMLInputElement>>('search');
constructor() {
// ❌ undefined here — too early
console.log(this.searchInput());
// ✅ runs once the view is in the DOM
afterNextRender(() => {
this.searchInput()?.nativeElement.focus();
});
// ✅ reacts whenever the queried element changes
effect(() => {
const el = this.searchInput();
if (el) console.log('Search input mounted', el.nativeElement);
});
}
}
contentChild for projected content #
A wrapping component can query content the parent passed in:
@Component({
selector: 'app-modal',
template: `
<header>
<ng-content select="[modal-title]" />
</header>
<main><ng-content /></main>
`,
})
export class ModalComponent {
// Query for the projected title element
title = contentChild<ElementRef<HTMLElement>>('modalTitle');
constructor() {
effect(() => {
const el = this.title();
if (el) console.log('Modal title:', el.nativeElement.textContent);
});
}
}
// Consumer
<app-modal>
<h2 modal-title #modalTitle>Confirm delete</h2>
<p>Are you sure?</p>
</app-modal>
contentChild works the same way as viewChild — same required option, same read option, same generic typing. The only difference is where it looks: own template (view) vs projected content (content).
A tabs component using all four #
A realistic case using both the singular and plural of view and content queries:
import { Component, contentChildren, viewChild, signal, effect, ElementRef } from '@angular/core';
@Component({
selector: 'app-tab',
template: `
@if (active()) {
<ng-content />
}
`,
})
export class TabComponent {
label = input.required<string>();
active = signal(false);
}
@Component({
selector: 'app-tabs',
imports: [TabComponent],
template: `
<nav #tabBar>
@for (t of tabs(); track t.label(); let i = $index) {
<button (click)="activeIdx.set(i)">{{ t.label() }}</button>
}
</nav>
<ng-content />
`,
})
export class TabsComponent {
tabs = contentChildren(TabComponent); // projected tabs
tabBar = viewChild<ElementRef<HTMLElement>>('tabBar'); // own template ref
activeIdx = signal(0);
constructor() {
effect(() => {
const tabs = this.tabs();
const idx = this.activeIdx();
tabs.forEach((tab, i) => tab.active.set(i === idx));
});
}
}
contentChildren(TabComponent) grabs every projected <app-tab>. viewChild('tabBar') grabs the wrapping component's own nav element. The effect synchronizes the active state across all tabs whenever either signal changes.
Decorator-based queries — what changed #
The legacy pattern, still seen in older code:
export class TabsComponent implements AfterViewInit, AfterContentInit {
@ViewChild('tabBar') tabBar!: ElementRef;
@ContentChildren(TabComponent) tabs!: QueryList<TabComponent>;
ngAfterViewInit() { /* use this.tabBar */ }
ngAfterContentInit() { /* iterate this.tabs */ }
}
Differences from signal queries:
| Aspect | Decorator queries | Signal queries |
|---|---|---|
| Result type | T or QueryList<T> |
Signal<T> or Signal<T[]> |
| Initialization | Set after lifecycle hook fires | Reactive — fire effect() automatically |
| Lifecycle pairing | Needs AfterViewInit / AfterContentInit |
No lifecycle hook needed |
| Required option | Couldn't enforce at compile time | viewChild.required<T>() enforces |
| Tree-shaking | Decorator metadata included | Pure functions, better tree-shaking |
The Angular CLI ships a migration: ng generate @angular/core:signal-queries-migration. Run it on any v17.2+ codebase.
Common gotchas #
| Symptom | Cause | Fix |
|---|---|---|
Signal returns undefined in constructor |
Template hasn't rendered yet | Move the read inside effect() or afterNextRender() |
viewChild for <ng-template> returns ElementRef, not TemplateRef |
Default read is ElementRef; templates need explicit hint | Add { read: TemplateRef } |
viewChildren array is empty on first effect run |
@for items haven't been created yet |
The effect re-runs when the queried list changes — second run will have the items |
contentChild empty even though projection works |
Used viewChild for projected content |
Switch to contentChild |
| Required query throws | The queried element really is missing from the template | Either ensure the element exists, or remove .required<>() and handle undefined |
What's next #
Lesson 2.14 closes Module 2 with programmatic component creation — ViewContainerRef.createComponent() for modal/portal patterns and dynamic component plug-ins. After that, Module 3 dives into the signal system in depth.
Try it yourself #
Auto-focus the first input on mount, using contentChild to find it in projected content:
@Component({
selector: 'app-form-card',
template: `
<section class="card">
<ng-content />
</section>
`,
})
export class FormCardComponent {
firstInput = contentChild<ElementRef<HTMLInputElement>>('firstInput');
constructor() {
afterNextRender(() => {
this.firstInput()?.nativeElement.focus();
});
}
}
// Consumer
<app-form-card>
<input #firstInput placeholder="Name" />
<input placeholder="Email" />
</app-form-card>
The wrapping card auto-focuses whichever input the consumer marks with #firstInput. The card has no knowledge of what is projected — it just queries for the reference.
get_best_practicesBecause the template hasn’t rendered yet. The constructor runs before the view exists. Move the read into afterNextRender(() => this.child()?.doThing()) for one-time access, or into effect(() => { const c = this.child(); if (c) ... }) for reactive access. The effect approach is best for queries that may change (a @for list, a conditional @if) — the effect re-fires when the query updates.Lesson 2.14 picks up programmatic component creation — the dynamic-rendering escape hatch.
Up next in Angular
More from this topic
Enjoyed this article?
Get new Angular tutorials delivered. No spam — just code-first articles when they ship.


