AngularJSViewChild & ContentChild

ViewChild & ContentChild in Angular

Angular provides four decorators for accessing elements, directives, or child components from a parent:

  • @ViewChild / @ViewChildren — access elements in the component's own template
  • @ContentChild / @ContentChildren — access elements projected into the component via <ng-content>

Understanding when each is available in the lifecycle is critical to using them correctly.

Quick Reference

Decorator

Targets

Available From

@ViewChild

Component's own template (child components, DOM elements, directives)

ngAfterViewInit

@ViewChildren

QueryList of all matching elements in own template

ngAfterViewInit

@ContentChild

First projected content element (passed via ng-content)

ngAfterContentInit

@ContentChildren

QueryList of all projected content elements

ngAfterContentInit

@ViewChild — Access a Child Component

TS
// src/app/child/child.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-child',
  standalone: true,
  template: `<p>Child: {{ message }}</p>`,
})
export class ChildComponent {
  message = 'Hello from child!';

  greet(name: string): void {
    this.message = `Hello, ${name}!`;
  }
}

// src/app/parent/parent.component.ts
import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { ChildComponent } from '../child/child.component';

@Component({
  selector: 'app-parent',
  standalone: true,
  imports: [ChildComponent],
  template: `
    <app-child />
    <button (click)="greetChild()">Greet Child</button>
  `,
})
export class ParentComponent implements AfterViewInit {
  @ViewChild(ChildComponent) child!: ChildComponent;

  ngAfterViewInit(): void {
    // Safe to access child here
    console.log(child.message); // 'Hello from child!'
  }

  greetChild(): void {
    this.child.greet('Angular');
  }
}
Warning
Never access @ViewChild in ngOnInit — the view hasn't been initialized yet. Use ngAfterViewInit or the static: true option for static references.
@ViewChild — Access a DOM Element

TS
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';

@Component({
  selector: 'app-input-focus',
  standalone: true,
  template: `
    <input #myInput type="text" placeholder="I'll auto-focus" />
    <button (click)="focusInput()">Focus</button>
  `,
})
export class InputFocusComponent implements AfterViewInit {
  @ViewChild('myInput') inputRef!: ElementRef<HTMLInputElement>;

  ngAfterViewInit(): void {
    this.inputRef.nativeElement.focus();
  }

  focusInput(): void {
    this.inputRef.nativeElement.focus();
    this.inputRef.nativeElement.select();
  }
}
Tip
Use template reference variables (#myInput) to target specific DOM elements or directives with @ViewChild.
static: true — For Elements Always in the Template

By default, @ViewChild is resolved after change detection. Set static: true when the element is never inside an @if or *ngIf — this makes it available in ngOnInit.

TS
@Component({
  template: `<canvas #chart></canvas>`, // always present, not behind *ngIf
})
export class ChartComponent implements OnInit {
  // static: true — available in ngOnInit
  @ViewChild('chart', { static: true }) canvas!: ElementRef<HTMLCanvasElement>;

  ngOnInit(): void {
    // Safe to use because static: true
    const ctx = this.canvas.nativeElement.getContext('2d');
    // initialize chart...
  }
}
@ViewChild with read — Access a Directive or ViewContainerRef

TS
import { Component, ViewChild, ViewContainerRef } from '@angular/core';

@Component({
  template: `<ng-template #host></ng-template>`,
})
export class DynamicHostComponent {
  // Get the ViewContainerRef of the ng-template, not the TemplateRef itself
  @ViewChild('host', { read: ViewContainerRef })
  host!: ViewContainerRef;

  loadComponent(): void {
    const ref = this.host.createComponent(SomeDynamicComponent);
    ref.setInput('data', 'hello');
  }
}
@ViewChildren — QueryList of Multiple Elements

TS
import { Component, ViewChildren, QueryList, AfterViewInit, ElementRef } from '@angular/core';

@Component({
  selector: 'app-items',
  standalone: true,
  template: `
    @for (item of items; track item) {
      <div #itemEl class="item">{{ item }}</div>
    }
  `,
})
export class ItemsComponent implements AfterViewInit {
  items = ['Apple', 'Banana', 'Cherry'];

  @ViewChildren('itemEl') itemElements!: QueryList<ElementRef<HTMLDivElement>>;

  ngAfterViewInit(): void {
    // Initial list
    console.log('Item count:', this.itemElements.length);

    // QueryList is live — fires on add/remove
    this.itemElements.changes.subscribe((list: QueryList<ElementRef>) => {
      console.log('Items changed, new count:', list.length);
    });
  }

  highlight(index: number): void {
    const el = this.itemElements.get(index);
    if (el) {
      el.nativeElement.style.background = 'yellow';
    }
  }
}
Note
QueryList is a live collection — it automatically updates when items are added or removed from the DOM. Subscribe to changes to react to those updates.
@ContentChild — Projected Content

@ContentChild lets a parent component access content that consumers project into it via <ng-content>.

TS
// src/app/components/card/card.component.ts
import { Component, ContentChild, AfterContentInit } from '@angular/core';
import { CardHeaderComponent } from './card-header.component';
import { CardFooterComponent } from './card-footer.component';

@Component({
  selector: 'app-card',
  standalone: true,
  template: `
    <div class="card">
      <div class="card-header">
        <ng-content select="app-card-header" />
      </div>
      <div class="card-body">
        <ng-content />
      </div>
      <div class="card-footer" *ngIf="hasFooter">
        <ng-content select="app-card-footer" />
      </div>
    </div>
  `,
})
export class CardComponent implements AfterContentInit {
  @ContentChild(CardHeaderComponent) header?: CardHeaderComponent;
  @ContentChild(CardFooterComponent) footer?: CardFooterComponent;

  hasFooter = false;

  ngAfterContentInit(): void {
    // Now we can access projected components
    this.hasFooter = !!this.footer;

    if (this.header) {
      console.log('Card has header:', this.header.title);
    }
  }
}

// Usage:
// <app-card>
//   <app-card-header title="My Card" />
//   <p>Card body content</p>
//   <app-card-footer>Save | Cancel</app-card-footer>
// </app-card>
@ContentChildren — All Projected Items

A common pattern is a tab group that reads all projected tab components.

TS
// src/app/components/tabs/tab.component.ts
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-tab',
  standalone: true,
  template: `
    @if (active) {
      <div class="tab-panel"><ng-content /></div>
    }
  `,
})
export class TabComponent {
  @Input() label = '';
  active = false;
}

// src/app/components/tabs/tab-group.component.ts
import {
  Component, ContentChildren, QueryList, AfterContentInit
} from '@angular/core';
import { TabComponent } from './tab.component';
import { NgFor } from '@angular/common';

@Component({
  selector: 'app-tab-group',
  standalone: true,
  imports: [NgFor],
  template: `
    <div class="tab-bar">
      <button
        *ngFor="let tab of tabs"
        [class.active]="tab.active"
        (click)="select(tab)"
      >{{ tab.label }}</button>
    </div>
    <ng-content />
  `,
})
export class TabGroupComponent implements AfterContentInit {
  @ContentChildren(TabComponent) tabs!: QueryList<TabComponent>;

  ngAfterContentInit(): void {
    // Activate the first tab
    const first = this.tabs.first;
    if (first) first.active = true;
  }

  select(selected: TabComponent): void {
    this.tabs.forEach(tab => (tab.active = false));
    selected.active = true;
  }
}

// Usage:
// <app-tab-group>
//   <app-tab label="Overview">Overview content</app-tab>
//   <app-tab label="Details">Details content</app-tab>
//   <app-tab label="Reviews">Reviews content</app-tab>
// </app-tab-group>
Signal-Based Queries (Angular 17+)

Angular 17 introduced signal-based query functions as an alternative to the decorator-based approach. These return signals instead of properties, integrating seamlessly with the signal ecosystem.

TS
import { Component, viewChild, contentChild, contentChildren, ElementRef } from '@angular/core';

@Component({
  selector: 'app-signal-queries',
  standalone: true,
  template: `
    <input #myInput />
    <ng-content />
  `,
})
export class SignalQueriesComponent {
  // viewChild<T>(locator) — returns Signal<T | undefined>
  input = viewChild<ElementRef>('myInput');

  // contentChild<T>(Type) — returns Signal<T | undefined>
  header = contentChild(HeaderComponent);

  // contentChildren<T>(Type) — returns Signal<readonly T[]>
  tabs = contentChildren(TabComponent);

  constructor() {
    // Use in effect() or computed() — no lifecycle hook needed
    effect(() => {
      const inputEl = this.input();
      if (inputEl) {
        inputEl.nativeElement.focus();
      }
    });
  }
}
Tip
Signal-based queries (viewChild(), contentChildren()) can be used in constructor or effect() without waiting for ngAfterViewInit — the signal simply returns undefined before initialization.
Comparison: Decorator vs Signal Queries

Decorator (@ViewChild)

Signal (viewChild())

Syntax

@ViewChild(ref) child!: T

child = viewChild<T>(ref)

Access

this.child (after view init)

this.child() (signal call)

Lifecycle

Must wait for ngAfterViewInit

Signal is undefined until ready

Reactivity

Manual change detection

Automatic with computed/effect

Angular version

All versions

Angular 17+

Best Practices
  • Access @ViewChild values in ngAfterViewInit, not ngOnInit

  • Use static: true only for elements that are never inside *ngIf/@if

  • Use { read: ViewContainerRef } to get a ViewContainerRef from a template ref

  • Subscribe to QueryList.changes to react to dynamic additions/removals

  • Prefer @ContentChild over @Input for structural child components (tabs, accordions)

  • In Angular 17+, prefer signal-based viewChild/contentChild for new code

  • Never manipulate DOM via ElementRef.nativeElement for data binding — use Angular bindings

Summary

@ViewChild, @ViewChildren, @ContentChild, and @ContentChildren give Angular components precise access to child components, directives, and DOM elements. The key distinction is the lifecycle hook — view queries are ready in ngAfterViewInit and content queries in ngAfterContentInit. Angular 17+ signal-based equivalents (viewChild(), contentChildren()) integrate natively with the signal reactivity system and are the recommended approach for new code.