ViewChild, ContentChild, and Template References

Angular’s component model normally hides a component’s DOM from its parent โ€” you interact with children through @Input()/@Output() APIs. But sometimes you genuinely need direct access to a DOM element or child component instance: focusing an input, measuring dimensions, calling a child component method imperatively, or reading a child’s signal values. @ViewChild, @ViewChildren, @ContentChild, and template reference variables are Angular’s mechanisms for these cases. Understanding when these are appropriate โ€” and when they indicate a design problem โ€” is important for building clean component architectures.

ViewChild and ContentChild Summary

Decorator Queries Available From Returns
@ViewChild(ref) Template element by ref var, component by type, or directive ngAfterViewInit Single match or undefined
@ViewChildren(ref) All template elements matching a selector ngAfterViewInit QueryList<T>
@ContentChild(ref) Projected content from parent (ng-content) ngAfterContentInit Single match or undefined
@ContentChildren(ref) All projected content matching a selector ngAfterContentInit QueryList<T>
viewChild() signal Signal-based ViewChild (Angular 17+) Reactive โ€” no hook needed Signal wrapping reference
contentChild() signal Signal-based ContentChild (Angular 17+) Reactive โ€” no hook needed Signal wrapping reference

ViewChild read Options

read Value Returns Use When
Default (no read) Component or directive instance if selector is a type; ElementRef if template ref Most cases
{ read: ElementRef } ElementRef wrapping the native DOM element Need raw DOM access
{ read: ViewContainerRef } ViewContainerRef at that location Dynamic component creation
{ read: TemplateRef } TemplateRef from a template ref variable Programmatic template rendering
Note: Angular 17 introduced signal-based query functions: viewChild(), viewChildren(), contentChild(), and contentChildren(). These return Signals rather than requiring a lifecycle hook โ€” they are reactive and can be used in computed() and effect(). A viewChild(ChildComponent) signal resolves after the view initialises and tracks the reference, making it safer to use than the lifecycle-hook-dependent @ViewChild.
Tip: Before reaching for @ViewChild to call a child component method imperatively, consider whether an @Input() driven approach would be cleaner. Instead of this.modalRef.open(), add an @Input() isOpen: boolean to the modal and let the parent control it declaratively. Imperative component control via @ViewChild is appropriate for DOM elements (focusing, measuring) and third-party library integrations, but it breaks the declarative data flow model for your own components.
Warning: @ViewChild references are undefined until ngAfterViewInit() runs. Accessing them in the constructor or ngOnInit() returns undefined and causes runtime errors. Always check for existence before use: if (this.myInput) { ... }. With the signal-based viewChild(), the signal value is undefined before the view initialises and the correct component instance after โ€” use it in effect() to safely react to when it becomes available.

Complete ViewChild and ContentChild Examples

// โ”€โ”€ @ViewChild โ€” access DOM element or child component โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
import {
    Component, OnInit, AfterViewInit, ViewChild, ViewChildren,
    ElementRef, QueryList, viewChild, viewChildren, signal,
} from '@angular/core';

@Component({
    selector:   'app-task-form',
    standalone: true,
    template: `
        <input #titleInput type="text" [value]="title()" placeholder="Task title">
        <textarea #descInput></textarea>
        <app-tag-input #tagInput [tags]="tags()"></app-tag-input>
        <app-task-card *ngFor="let task of tasks()" [task]="task"></app-task-card>
    `,
})
export class TaskFormComponent implements AfterViewInit {
    title = signal('');
    tags  = signal<string[]>([]);
    tasks = signal<Task[]>([]);

    // โ”€โ”€ Decorator-based ViewChild โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

    // By template reference variable '#titleInput'
    @ViewChild('titleInput') titleInput!: ElementRef<HTMLInputElement>;

    // By component type โ€” returns the component instance
    @ViewChild(TagInputComponent) tagInput!: TagInputComponent;

    // Read as ElementRef explicitly (when querying a component)
    @ViewChild(TagInputComponent, { read: ElementRef }) tagInputEl!: ElementRef;

    // Query all TaskCardComponents
    @ViewChildren(TaskCardComponent) taskCards!: QueryList<TaskCardComponent>;

    // โ”€โ”€ Signal-based ViewChild (Angular 17+) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

    titleInputSignal = viewChild<ElementRef>('titleInput');
    tagInputSignal   = viewChild(TagInputComponent);
    taskCardsSignal  = viewChildren(TaskCardComponent);

    ngAfterViewInit(): void {
        // Safe to access @ViewChild here
        this.titleInput.nativeElement.focus();

        // Iterate all task cards
        this.taskCards.forEach(card => {
            console.log('Card for:', card.task()._id);
        });

        // Listen to QueryList changes (when items added/removed by *ngFor)
        this.taskCards.changes.subscribe(cards => {
            console.log('Task cards changed, count:', cards.length);
        });
    }

    // With signal viewChild โ€” no lifecycle hook needed
    focusTitle(): void {
        this.titleInputSignal()?.nativeElement.focus();
    }

    // Call child component method imperatively (use sparingly)
    clearTags(): void {
        this.tagInputSignal()?.clearAll();
    }
}

// โ”€โ”€ Template Reference Variables (#ref) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@Component({
    standalone: true,
    template: `
        <!-- #ref on native element: refers to HTMLInputElement -->
        <input #searchInput type="search" (input)="onSearch(searchInput.value)">
        <button (click)="searchInput.focus()">Focus Search</button>
        <button (click)="searchInput.value = ''">Clear</button>

        <!-- #ref on component: refers to component instance -->
        <app-dropdown #dropdown [options]="options"></app-dropdown>
        <button (click)="dropdown.open()">Open Dropdown</button>

        <!-- #ref on ng-template: refers to TemplateRef -->
        <ng-template #emptyTpl>
            <p>No tasks found.</p>
        </ng-template>

        <!-- Use the template ref with *ngIf else -->
        <ul *ngIf="tasks().length > 0; else emptyTpl">
            <li *ngFor="let t of tasks()">{{ t.title }}</li>
        </ul>
    `,
})
export class SearchPageComponent { ... }

// โ”€โ”€ @ContentChild โ€” access projected content โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@Component({
    selector: 'app-card',
    standalone: true,
    template: `
        <div class="card">
            <div class="card__header">
                <!-- Projected header content -->
                <ng-content select="[cardHeader]"></ng-content>
            </div>
            <div class="card__body">
                <!-- Default projected content -->
                <ng-content></ng-content>
            </div>
        </div>
    `,
})
export class CardComponent {
    // Access the projected header element
    @ContentChild('cardHeader') header?: ElementRef;

    ngAfterContentInit(): void {
        if (this.header) {
            console.log('Header projected:', this.header.nativeElement.textContent);
        }
    }
}

// Parent usage:
// <app-card>
//     <h2 cardHeader>My Card Title</h2>  <!-- projected into header slot -->
//     <p>Card body content</p>           <!-- projected into default slot -->
// </app-card>

How It Works

Step 1 โ€” Template Reference Variables Are Local Template Aliases

A #ref variable on an element creates a local alias in the template scope. On a native HTML element (<input #myInput>), the ref refers to the HTMLInputElement DOM object. On a component (<app-dropdown #dd>), it refers to the component class instance โ€” giving direct access to its public properties and methods. These refs are only accessible within the same template; to use them in the component class, use @ViewChild.

Step 2 โ€” @ViewChild Queries Are Resolved After View Initialisation

Angular’s template compiler sets up ViewChild queries during the view initialisation phase. After calling ngOnInit(), Angular renders the component’s template and child components. Once rendering is complete, it resolves all @ViewChild queries and sets the decorated properties. This is why ngAfterViewInit() is the earliest safe point to access ViewChild references. The signal-based viewChild() follows the same resolution timing but exposes the result as a reactive Signal.

Step 3 โ€” ViewChildren and QueryList Track Dynamic Content

@ViewChildren(TaskCardComponent) returns a QueryList โ€” a live, observable collection that automatically updates when the template’s dynamic content changes (items added or removed by *ngFor). Subscribe to queryList.changes to react to these changes. The signal-based viewChildren() returns a Signal<readonly T[]> โ€” reactive by default, no subscription management needed.

Step 4 โ€” ContentChild Accesses Projected External Content

While @ViewChild queries a component’s own template, @ContentChild queries content projected into it from the parent through <ng-content>. A card component that accepts projected header content can access and read that header’s DOM element with @ContentChild. The projected content is not part of the card’s own template โ€” it belongs to the parent โ€” so it is only available in ngAfterContentInit(), not ngAfterViewInit().

Step 5 โ€” Signal Queries Are Simpler and Safer

Signal-based queries (viewChild(), viewChildren()) return Signals that are undefined before the view initialises and the resolved reference after. This removes the need for the ngAfterViewInit() lifecycle hook in many cases. Use them in effect() to run code when the reference becomes available: effect(() => { if (this.myInput()) { this.myInput()!.nativeElement.focus(); } }). The optional chaining handles the undefined state naturally.

Common Mistakes

Mistake 1 โ€” Accessing @ViewChild in ngOnInit

โŒ Wrong โ€” view not rendered yet:

@ViewChild('myInput') myInput!: ElementRef;

ngOnInit(): void {
    this.myInput.nativeElement.focus();  // TypeError: Cannot read property 'nativeElement' of undefined
}

✅ Correct โ€” use ngAfterViewInit:

ngAfterViewInit(): void {
    this.myInput?.nativeElement.focus();  // defined here
}

Mistake 2 โ€” Overusing ViewChild to call child methods instead of @Input()

โŒ Wrong โ€” imperative control breaks declarative data flow:

@ViewChild(ModalComponent) modal!: ModalComponent;

showDeleteConfirm(): void {
    this.modal.title = 'Delete Task?';
    this.modal.open();   // calling child method imperatively
}

✅ Better โ€” drive child state through @Input():

modalConfig = signal({ isOpen: false, title: '' });

showDeleteConfirm(): void {
    this.modalConfig.set({ isOpen: true, title: 'Delete Task?' });
}
// Template: <app-modal [isOpen]="modalConfig().isOpen" [title]="modalConfig().title">

Mistake 3 โ€” Not unsubscribing from QueryList.changes

โŒ Wrong โ€” QueryList.changes subscription leaks on destroy:

ngAfterViewInit(): void {
    this.taskCards.changes.subscribe(cards => { ... });  // never unsubscribed
}

✅ Correct โ€” pipe through takeUntilDestroyed:

ngAfterViewInit(): void {
    this.taskCards.changes
        .pipe(takeUntilDestroyed())
        .subscribe(cards => { ... });
}

Quick Reference

Need Decorator/Function Available From
Single DOM element @ViewChild('ref') el!: ElementRef ngAfterViewInit
Single component instance @ViewChild(ChildComp) child!: ChildComp ngAfterViewInit
All matching components @ViewChildren(ChildComp) list!: QueryList<ChildComp> ngAfterViewInit
Projected content @ContentChild('ref') header?: ElementRef ngAfterContentInit
Signal ViewChild child = viewChild(ChildComp) Signal (reactive)
Signal ViewChildren children = viewChildren(ChildComp) Signal array (reactive)
Template ref variable <input #myRef> โ†’ used in template only Within template
Class-level template ref @ViewChild('myRef') myRef!: ElementRef ngAfterViewInit

🧠 Test Yourself

A component uses @ViewChild('emailInput') emailInput!: ElementRef. In ngOnInit(), this.emailInput is undefined. Why, and what is the fix?