Angular 70 🅰️ ViewChild, ViewChildren, and ContentChild
Angular components rarely live in isolation. A parent component often needs to reach into its own template and grab a reference to a child element, a directive instance, or a component. It may also need to inspect or manipulate content that a caller has projected into it via <ng-content>. Angular’s query system provides four tools for this: @ViewChild, @ViewChildren, @ContentChild, and @ContentChildren. These decorators let you access elements after Angular has created the view, and they are the backbone of imperative DOM manipulation, focus management, and parent-child coordination.
This chapter covers the decorator-based query APIs and their signal-based counterparts — viewChild, viewChildren, contentChild, and contentChildren — which are now the recommended approach in modern Angular. You will learn how view and content queries differ, when each resolves, how to handle the undefined case, and the lifecycle hooks that guarantee query results are available.
One important context: signal queries were introduced in Angular 17.2 as a developer preview and became production-ready in v19. The Angular team provides an automated migration from decorator queries to signal queries. For new projects, signal queries are the standard.
Key point: View queries look at a component’s own template. Content queries look at projected content passed into the component via <ng-content>. Queries never pierce through component boundaries — a parent cannot query into a child component’s internal template, and a child cannot query into the parent’s template. This encapsulation is deliberate.
Why queries exist
Angular’s declarative model handles most interactions. Inputs pass data down, outputs emit events up, and services share state. But some scenarios require direct references.
The DOM manipulation problem. A component may need to focus an input on load, read an element’s dimensions, or call a method on a ViewContainerRef. Template bindings can’t do this. A query gives you the ElementRef or component instance to call imperative APIs.
The coordination problem. A parent component may need to call methods on its children — a form that validates all its controls, a list that scrolls to a selected item, a tab group that activates a specific tab. Queries give the parent typed references to those children.
The projected content problem. A reusable component like a card or modal accepts content from its caller. The component may need to inspect that content — apply styles conditionally, count items, or find a specific directive. Content queries let the component see what was projected into it.
The reactivity problem. Decorator queries return a static reference. If the underlying element changes — because an @if block toggles, or a list re-renders — you must manually re-check. Signal queries return a reactive signal that updates automatically.
The trade-off. Queries encourage imperative patterns. Reaching into a child component’s internals couples the parent to the child’s implementation. Use queries for DOM interaction and lifecycle coordination, not for data flow. When possible, prefer inputs and outputs for communication.
a. View Queries: @ViewChild and @ViewChildren
View queries inspect a component’s own template. When you declare @ViewChild('inputRef') input: ElementRef, Angular searches the component’s template for an element with the #inputRef template reference variable and assigns it to the property.
@ViewChild returns a single result — the first match if multiple elements match. @ViewChildren returns a QueryList, which is an iterable of all matches and updates when children are added or removed.
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-search',
template: `
<input #searchInput type="text" placeholder="Search..." />
<button (click)="focusInput()">Focus</button>
`,
})
export class SearchComponent implements AfterViewInit {
@ViewChild('searchInput') searchInput!: ElementRef<HTMLInputElement>;
ngAfterViewInit() {
this.searchInput.nativeElement.focus();
}
focusInput() {
this.searchInput.nativeElement.focus();
}
}
The @ViewChild decorator accepts a template reference variable as a string, a component or directive class, or a TemplateRef. The read option lets you request a different token from the matched element — for example, read: ElementRef when the selector matches a component but you want the host element.
The timing rule is critical. View queries resolve after Angular creates the component’s view. Accessing @ViewChild in the constructor or ngOnInit returns undefined. The results are available in ngAfterViewInit. If you set static: true, the query resolves before change detection runs (available in ngOnInit), but it will never update if the element appears later via @if or @for.
@ViewChildren returns a QueryList<T> rather than a single value. The list updates automatically when the template changes — items added by @for appear, removed items disappear. This makes it suitable for tracking dynamic collections.
@ViewChildren(ChildComponent) children!: QueryList<ChildComponent>;
ngAfterViewInit() {
console.log(this.children.length); // current count
this.children.changes.subscribe((list) => {
console.log('changed:', list.length);
});
}
b. Content Queries: @ContentChild and @ContentChildren
Content queries inspect projected content — the markup a caller places between a component’s opening and closing tags. While view queries look at what the component itself wrote, content queries look at what was passed to the component.
@Component({
selector: 'app-card',
template: `
<div class="card">
<ng-content></ng-content>
</div>
`,
})
export class CardComponent {
@ContentChild('title') title?: ElementRef;
}
A parent uses it like this:
<app-card>
<h2 #title>My Title</h2>
</app-card>
The #title reference is authored in the parent’s template, but the CardComponent can query it via @ContentChild. This is the fundamental difference: view queries find what the component owns; content queries find what the caller supplied.
The timing rule is different. Content queries resolve after Angular projects the content into the component. The results are available in ngAfterContentInit, which runs before ngAfterViewInit. Accessing a content query in the constructor or ngOnInit returns undefined.
import { ContentChild, AfterContentInit } from '@angular/core';
@Component({ /* ... */ })
export class CardComponent implements AfterContentInit {
@ContentChild('title') title?: ElementRef;
ngAfterContentInit() {
console.log(this.title?.nativeElement.textContent);
}
}
The descendant limitation is important. By default, content queries find only direct children of the component and do not traverse into descendants. If the projected content is wrapped in another element, the query won’t find it unless you specify descendants: true in the options (available for @ContentChildren).
@ContentChildren returns a QueryList<T> of all matching projected elements. It is commonly used to build reusable components that need to inspect or coordinate multiple projected items — a tab group that counts its panels, a list that styles its items.
@ContentChildren(TabPanelComponent) panels!: QueryList<TabPanelComponent>;
c. Signal Queries: The Modern API
Signal queries — viewChild, viewChildren, contentChild, contentChildren — replace the decorator APIs with functions that return signals. They were introduced in Angular 17.2 and are production-ready as of v19. The Angular team provides an automated migration.
import { Component, viewChild, ElementRef, afterNextRender } from '@angular/core';
@Component({
selector: 'app-search',
template: `<input #searchInput />`,
})
export class SearchComponent {
searchInput = viewChild<ElementRef>('searchInput');
constructor() {
afterNextRender(() => {
this.searchInput()?.nativeElement.focus();
});
}
}
The signal returns undefined if no match is found. Calling the signal reads the current result. When the underlying element changes — because @if toggles, or @for re-renders — the signal updates automatically, and any computed or effect that depends on it re-runs.
For cases where you know the element must exist, use the .required() variant:
searchInput = viewChild.required<ElementRef>('searchInput');
This removes undefined from the type. If the query finds no match, Angular throws an error (NG0951). Use it when the element is always present — a template that doesn’t wrap the target in control flow.
The multiple-result variants return signals over arrays, not QueryList:
children = viewChildren(ChildComponent); // Signal<readonly ChildComponent[]>
When no matches are found, the array is empty, not undefined. This guarantees the result is always safely iterable.
The lifecycle rule still applies. Signal queries resolve at the same points as their decorator counterparts. viewChild results are available after the view is created; contentChild results are available after the content is projected. Accessing them in the constructor throws if the query is required. Use afterNextRender or afterRender for DOM work in signal-based components.
Complete Example Session
This session builds a search component, a card with projected content, and demonstrates both decorator and signal queries.
// ============================================
// PART 1: THE VIEWCHILD DECORATOR
// ============================================
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-search',
template: `
<input #searchInput type="text" placeholder="Search..." />
<button (click)="focusInput()">Focus</button>
`,
})
export class SearchComponent implements AfterViewInit {
@ViewChild('searchInput') searchInput!: ElementRef<HTMLInputElement>;
ngAfterViewInit() {
this.searchInput.nativeElement.focus();
}
focusInput() {
this.searchInput.nativeElement.focus();
}
}
// ============================================
// PART 2: THE VIEWCHILD SIGNAL
// ============================================
import { Component, viewChild, ElementRef, afterNextRender } from '@angular/core';
@Component({
selector: 'app-search-signal',
template: `<input #searchInput type="text" />`,
})
export class SearchSignalComponent {
searchInput = viewChild<ElementRef<HTMLInputElement>>('searchInput');
constructor() {
afterNextRender(() => {
this.searchInput()?.nativeElement.focus();
});
}
}
// ============================================
// PART 3: THE VIEWCHILDREN DECORATOR
// ============================================
import { ViewChildren, QueryList } from '@angular/core';
@Component({
selector: 'app-list',
template: `
@for (item of items; track item.id) {
<app-item [item]="item" />
}
`,
})
export class ListComponent {
@ViewChildren(ItemComponent) items!: QueryList<ItemComponent>;
items = [{ id: 1 }, { id: 2 }];
ngAfterViewInit() {
console.log(this.items.length);
}
}
// ============================================
// PART 4: THE CONTENTCHILD DECORATOR
// ============================================
import { ContentChild, AfterContentInit } from '@angular/core';
@Component({
selector: 'app-card',
template: `<div class="card"><ng-content></ng-content></div>`,
})
export class CardComponent implements AfterContentInit {
@ContentChild('title') title?: ElementRef;
ngAfterContentInit() {
console.log(this.title?.nativeElement.textContent);
}
}
// ============================================
// PART 5: THE CONTENTCHILDREN DECORATOR
// ============================================
import { ContentChildren, QueryList } from '@angular/core';
@Component({
selector: 'app-tab-group',
template: `<ng-content></ng-content>`,
})
export class TabGroupComponent {
@ContentChildren(TabPanelComponent) panels!: QueryList<TabPanelComponent>;
ngAfterContentInit() {
console.log(`Found ${this.panels.length} panels`);
}
}
// ============================================
// PART 6: THE SIGNAL VIEWCHILD REQUIRED
// ============================================
searchInput = viewChild.required<ElementRef>('searchInput');
// Throws if #searchInput is not found
// ============================================
// PART 7: THE SIGNAL VIEWCHILDREN
// ============================================
import { viewChildren } from '@angular/core';
children = viewChildren(ChildComponent);
// Signal<readonly ChildComponent[]>
// Empty array if none found
// ============================================
// PART 8: THE SIGNAL CONTENTCHILD
// ============================================
import { contentChild } from '@angular/core';
title = contentChild<ElementRef>('title');
// Signal<ElementRef | undefined>
// ============================================
// PART 9: THE LIFECYCLE ORDER
// ============================================
// Order of execution:
// 1. constructor (queries NOT available)
// 2. ngOnInit (static: true view queries available)
// 3. ngAfterContentInit (content queries available)
// 4. ngAfterViewInit (view queries available)
// ============================================
// PART 10: THE READ OPTION
// ============================================
// Selector matches a component, but we want the host ElementRef
@ViewChild('myComponent', { read: ElementRef }) el!: ElementRef;
// Selector matches a component, but we want the TemplateRef
@ContentChild(TabPanelContentDirective, { read: TemplateRef }) tpl!: TemplateRef<unknown>;
The ten parts cover the @ViewChild decorator, the viewChild signal, @ViewChildren, @ContentChild, @ContentChildren, viewChild.required, viewChildren, contentChild, lifecycle order, and the read option.
Quick Reference
The Query APIs
| API | Returns | Scope | Available In |
|---|---|---|---|
@ViewChild | Single or undefined | Own template | ngAfterViewInit |
@ViewChildren | QueryList<T> | Own template | ngAfterViewInit |
@ContentChild | Single or undefined | Projected content | ngAfterContentInit |
@ContentChildren | QueryList<T> | Projected content | ngAfterContentInit |
viewChild | `Signal<T | undefined>` | Own template |
viewChildren | Signal<readonly T[]> | Own template | On read |
contentChild | `Signal<T | undefined>` | Projected content |
contentChildren | Signal<readonly T[]> | Projected content | On read |
The Selector Types
| Selector | Example |
|---|---|
| Template ref variable | @ViewChild('inputRef') |
| Component/directive class | @ViewChild(MyComponent) |
| Provider token | @ViewChild(SomeService) |
TemplateRef | @ViewChild(TemplateRef) |
The Read Options
| Read Value | Purpose |
|---|---|
ElementRef | The host DOM element |
ViewContainerRef | The view container |
TemplateRef | The template reference |
| Any provider token | The provider instance |
Best Practices
✅ Do This:
// Use signal queries for new code
searchInput = viewChild<ElementRef>('searchInput'); // ✅
// Use required when the element is guaranteed
searchInput = viewChild.required<ElementRef>('searchInput'); // ✅
// Access view queries in ngAfterViewInit
ngAfterViewInit() { this.searchInput.nativeElement.focus(); } // ✅
// Access content queries in ngAfterContentInit
ngAfterContentInit() { console.log(this.title()); } // ✅
// Use afterNextRender for DOM work in signal components
afterNextRender(() => this.searchInput()?.nativeElement.focus()); // ✅
// Use read when you need a different token
@ViewChild('cmp', { read: ElementRef }) el!: ElementRef; // ✅
❌ Don’t Do This:
// Don't access view queries in the constructor
constructor() { this.searchInput.nativeElement.focus(); } // ❌ undefined
// Don't access content queries in ngOnInit
ngOnInit() { console.log(this.title?.nativeElement); } // ❌ undefined
// Don't use static: true if the element can appear dynamically
@ViewChild('input', { static: true }) input!: ElementRef; // ❌ won't update
// Don't query across component boundaries
// A parent cannot query into a child's template. // ❌
// Don't use queries for data flow
// Use @Input and @Output instead. // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
undefined in ngOnInit | View queries resolve after view creation | Use ngAfterViewInit |
undefined in ngAfterViewInit | Content queries resolve earlier | Use ngAfterContentInit |
viewChild.required throws | Element hidden by @if or @for | Remove .required() or ensure element is always present |
@ViewChildren empty | Elements not yet rendered | Wait for ngAfterViewInit; use changes for updates |
@ContentChild finds nothing | Projected content wrapped in another element | Add descendants: true (for @ContentChildren) |
| Query result stale | Used static: true with dynamic content | Remove static: true |
Real-World Examples
1. Focus an Input on Load
searchInput = viewChild.required<ElementRef>('searchInput');
constructor() { afterNextRender(() => this.searchInput().nativeElement.focus()); }
2. Read Projected Content Text
title = contentChild.required<ElementRef>('title');
ngAfterContentInit() { console.log(this.title().nativeElement.textContent); }
3. Count Dynamic Children
children = viewChildren(ChildComponent);
count = computed(() => this.children().length);
4. Coordinate Tab Panels
panels = contentChildren(TabPanelComponent);
5. Access a Child Component Method
child = viewChild.required(ChildComponent);
ngAfterViewInit() { this.child().refresh(); }
6. Query a Directive Instance
@ContentChildren(MyMarkerDirective) markers!: QueryList<MyMarkerDirective>;
7. Read a Different Token
@ViewChild('cmp', { read: ElementRef }) el!: ElementRef;
8. Track ViewChildren Changes
this.children.changes.subscribe(list => console.log(list.length));
9. Required Content Query
title = contentChild.required('title');
10. Signal Query in a Computed
label = computed(() => this.title()?.nativeElement.textContent ?? 'Untitled');
Visual: View vs Content Scope
┌──────────────────────────────────────────────────────┐
│ PARENT TEMPLATE │
│ │
│ <app-card> │
│ <h2 #title>Projected</h2> ← CONTENT QUERY finds │
│ </app-card> │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ CARD TEMPLATE │ │
│ │ │ │
│ │ <input #inputRef /> ← VIEW QUERY finds │ │
│ │ <ng-content></ng-content> │ │
│ │ │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ View query: looks at card's own template │
│ Content query: looks at what parent projected │
└──────────────────────────────────────────────────────┘
Visual: Lifecycle Resolution Order
┌──────────────────────────────────────────────┐
│ LIFECYCLE RESOLUTION ORDER │
│ │
│ 1. constructor │
│ └─ Queries NOT available │
│ │
│ 2. ngOnInit │
│ └─ static: true view queries available │
│ │
│ 3. ngAfterContentInit │
│ └─ Content queries available │
│ │
│ 4. ngAfterViewInit │
│ └─ View queries available │
│ │
│ Signal queries follow the same timing. │
│ Read them on or after the appropriate hook. │
└──────────────────────────────────────────────┘
Visual: Signal Query State
┌──────────────────────────────────────────────┐
│ SIGNAL QUERY TYPES │
│ │
│ viewChild('ref') │
│ └─ Signal<ElementRef | undefined> │
│ │
│ viewChild.required('ref') │
│ └─ Signal<ElementRef> │
│ └─ Throws if not found │
│ │
│ viewChildren(Comp) │
│ └─ Signal<readonly Comp[]> │
│ └─ Empty array if none found │
│ │
│ contentChild('ref') │
│ └─ Signal<ElementRef | undefined> │
│ │
│ contentChild.required('ref') │
│ └─ Signal<ElementRef> │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| View query scope | Component’s own template |
| Content query scope | Projected content |
| View query timing | ngAfterViewInit |
| Content query timing | ngAfterContentInit |
| Signal query return | Signal<T> |
| Required query | .required() variant |
| Multiple results | QueryList (decorator) / readonly T[] (signal) |
read option | Request a different token |
static: true | Resolve before change detection, never updates |
Key takeaways:
- View queries look at a component’s own template; content queries look at projected content.
@ViewChildand@ViewChildreninspect what the component itself wrote.@ContentChildand@ContentChildreninspect what the caller passed via<ng-content>. - Queries never pierce component boundaries. A parent cannot query into a child’s template, and a child cannot query into the parent’s. This encapsulation is deliberate.
- Timing matters. View queries resolve in
ngAfterViewInit. Content queries resolve inngAfterContentInit. Accessing either in the constructor orngOnInitreturnsundefined. - Signal queries are the modern API.
viewChild,viewChildren,contentChild, andcontentChildrenreturn reactive signals that update automatically when the underlying elements change. They are production-ready as of v19. - Use
.required()when the element is guaranteed. This removesundefinedfrom the type and throws if the query finds nothing (NG0951). - The
readoption changes what you get. When a selector matches a component but you want itsElementRef,TemplateRef, orViewContainerRef, pass the token toread. - Don’t use queries for data flow. Use
@Inputand@Outputfor communication. Queries are for DOM interaction, focus management, and lifecycle coordination.
Remember: Queries are escape hatches from Angular’s declarative model. They let you reach into the view and content to do things templates can’t — focus an input, measure an element, call a child method, inspect projected content. View queries see what you wrote; content queries see what you were given. Signal queries make both reactive and type-safe. Use them when the declarative approach falls short, but prefer inputs and outputs for the data flow that drives your application.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!