Angular Programmatic Component Creation: ViewContainerRef.createComponent() for Modals, Toasts, Plugins [2026]
Most Angular components appear in the DOM because some other component's template declares them. But sometimes you need to render a component you do not know about at compile time — a dialog whose type is decided at runtime, a toast notification that fires from a service, a plugin a host app loads dynamically. For those cases Angular gives you ViewContainerRef.createComponent() — the programmatic-rendering API.
This is lesson 2.14 of the Angular Tutorial, closing Module 2. The patterns here are advanced — most application code never needs them. But when you need them (modals, toasts, dynamic forms, plugin systems), nothing else works.
The two ways components reach the DOM #
A quick mental model:
| Mechanism | When you use it | How |
|---|---|---|
| Template-driven | Static, known-at-compile-time UI | <app-card /> in a template |
| Programmatic | Dynamic, runtime-decided UI | vcr.createComponent(SomeClass) in code |
99% of components are template-driven. The remaining 1% are what this lesson is about.
The minimal example #
Given any component class, you can render it into a ViewContainerRef:
import { Component, ViewContainerRef, inject } from '@angular/core';
import { ToastComponent } from './toast.component';
@Component({
selector: 'app-root',
template: `
<button (click)="showToast()">Show toast</button>
<ng-container #toastContainer />
`,
})
export class AppRootComponent {
private vcr = inject(ViewContainerRef);
showToast() {
const ref = this.vcr.createComponent(ToastComponent);
ref.setInput('message', 'Saved!');
setTimeout(() => ref.destroy(), 3000);
}
}
What happened:
inject(ViewContainerRef)gives you the root component's view container.createComponent(ToastComponent)instantiates a newToastComponentand appends it.ref.setInput('message', 'Saved!')sets the new component's input signal.ref.destroy()removes it from the DOM and cleans up.
The returned ComponentRef<ToastComponent> is the handle for further interaction.
Choosing where to render #
Injecting ViewContainerRef from the component class gives you the enclosing container. To target a specific spot, query a <ng-container> reference:
@Component({
template: `
<header>...</header>
<main>
<ng-container #modalSlot />
</main>
`,
})
export class AppRootComponent {
modalSlot = viewChild('modalSlot', { read: ViewContainerRef });
show() {
const slot = this.modalSlot();
if (!slot) return;
const ref = slot.createComponent(SomeDialogComponent);
// ref lives at the marker position, not at the component root
}
}
The read: ViewContainerRef option on the signal query is the trick — it gives you the view container at that template position rather than the ElementRef. Now createComponent renders into that exact spot.
Setting inputs and reading outputs #
The ComponentRef exposes typed methods for both:
const ref = vcr.createComponent(ConfirmDialogComponent);
ref.setInput('title', 'Delete user?');
ref.setInput('confirmLabel', 'Delete');
ref.instance.confirmed.subscribe(() => {
this.deleteUser();
ref.destroy();
});
ref.instance.cancelled.subscribe(() => ref.destroy());
For outputs, you can subscribe directly to ComponentRef.instance.outputName — even though the modern output() doesn't expose .subscribe() on its own, ComponentRef exposes a compatible interface for programmatically-created components.
Lifecycle of a dynamically-created component #
Creating + destroying:
const ref = vcr.createComponent(MyComponent); // Component instantiated; CD runs
// ... user interacts ...
ref.destroy(); // Component removed from DOM, ngOnDestroy fires
No other lifecycle differences. The component goes through the full lifecycle (constructor, effect() setup, afterNextRender, etc.) exactly like a template-rendered one.
If the host component is destroyed before the dynamic child, Angular cleans up both. So programmatic components NEVER leak — the worst case is they survive too long until the host dies.
A complete pattern: a Modal service #
The classic use case — a service that opens modals from anywhere:
import { ApplicationRef, ComponentRef, EnvironmentInjector, Injectable, Type, createComponent, inject } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class ModalService {
private appRef = inject(ApplicationRef);
private injector = inject(EnvironmentInjector);
open<T extends object>(component: Type<T>, inputs: Partial<T> = {}): ComponentRef<T> {
// Create the component using the root-level createComponent helper
const ref = createComponent(component, {
environmentInjector: this.injector,
});
// Apply inputs
for (const [k, v] of Object.entries(inputs)) {
ref.setInput(k, v);
}
// Attach to the application — this triggers change detection
this.appRef.attachView(ref.hostView);
// Append the host element to the DOM
document.body.appendChild(ref.location.nativeElement);
// Provide a cleanup helper
const originalDestroy = ref.destroy.bind(ref);
ref.destroy = () => {
this.appRef.detachView(ref.hostView);
originalDestroy();
};
return ref;
}
}
Usage from any component:
const ref = this.modals.open(ConfirmDialogComponent, {
title: 'Delete user?',
body: 'This cannot be undone.',
confirmLabel: 'Delete',
});
ref.instance.confirmed.subscribe(() => {
this.deleteUser();
ref.destroy();
});
This pattern — services that open components via the DOM — underpins every Angular modal/toast/snackbar library. Material's MatDialog, PrimeNG's DialogService, ng-bootstrap's NgbModal are all variations on this.
createComponent() vs ViewContainerRef.createComponent() #
Two APIs do similar things:
| API | Use when |
|---|---|
ViewContainerRef.createComponent(Cmp) |
You have a specific anchor point in a template (a <ng-container #x>). Inserts AT that location. |
createComponent(Cmp, { environmentInjector, ... }) |
You are NOT in a template (e.g., a service). Creates standalone — you handle DOM insertion. |
For in-template insertion, prefer ViewContainerRef. For service-from-anywhere modals, prefer the bare createComponent().
Dynamic component selection by data #
A common pattern — render different components depending on data:
type PluginConfig =
| { type: 'chart'; data: ChartData }
| { type: 'table'; data: TableData }
| { type: 'image'; src: string };
const componentMap = {
chart: ChartPluginComponent,
table: TablePluginComponent,
image: ImagePluginComponent,
};
@Component({
template: `<ng-container #host />`,
})
export class PluginRendererComponent {
config = input.required<PluginConfig>();
host = viewChild('host', { read: ViewContainerRef });
private currentRef?: ComponentRef<unknown>;
constructor() {
effect(() => {
this.currentRef?.destroy();
const slot = this.host();
if (!slot) return;
const cfg = this.config();
const Cmp = componentMap[cfg.type];
this.currentRef = slot.createComponent(Cmp);
for (const [k, v] of Object.entries(cfg)) {
if (k !== 'type') this.currentRef.setInput(k, v);
}
});
}
}
The effect destroys the previous instance, looks up the right component class by type, instantiates it, and pipes the config inputs in. This is the cleanest pattern for plugin systems where the component class is decided at runtime.
When you do NOT need programmatic creation #
Three cases that look like they need it but don't:
- Conditional rendering —
@if (showModal()) { <app-modal ... /> }is simpler than programmatic creation - List of same component —
@for (item of items()) { <app-card [item]="item" /> }handles this natively - Switch between known components —
@switch (type) { @case ('chart') { <app-chart /> } @case ('table') { <app-table /> } }is more readable than a component map
Reach for createComponent only when the component class itself is dynamic (unknown at template authoring time) or when the rendering location is outside any normal template (modals attached to document.body).
Common gotchas #
| Symptom | Cause | Fix |
|---|---|---|
| Created component doesn't appear | Forgot to call appRef.attachView(ref.hostView) AND insert the element into the DOM |
Both steps are needed — check both |
| Inputs don't apply | Used ref.instance.input = value instead of ref.setInput('input', value) |
setInput is required to fire the input signal correctly |
| Change detection doesn't run | Component is detached from the application | appRef.attachView() is what enrolls it in change detection |
| Memory leak when component dies | Forgot to call ref.destroy() when the user dismisses the modal |
Always pair create with destroy — usually in the close/cancel handlers |
createComponent throws "No EnvironmentInjector" |
Called the bare createComponent without { environmentInjector } |
Provide one: inject(EnvironmentInjector) from the calling service |
What's next #
This closes Module 2 — Components & Templates. You can now write components in every shape Angular supports: from a 20-line <app-counter> with one input, all the way to a plugin-system component that decides what to render at runtime.
Module 3 picks up the signal system in depth — producer/consumer graph, effect() vs computed(), model(), linkedSignal(), zoneless change detection internals. Inputs, queries, and outputs are all signals; understanding the system unlocks the rest of Angular.
Try it yourself #
A tiny toast service in 40 lines:
// toast.service.ts
import { ApplicationRef, ComponentRef, createComponent, EnvironmentInjector, Injectable, inject } from '@angular/core';
import { ToastComponent } from './toast.component';
@Injectable({ providedIn: 'root' })
export class ToastService {
private appRef = inject(ApplicationRef);
private injector = inject(EnvironmentInjector);
show(message: string, duration = 3000) {
const ref = createComponent(ToastComponent, { environmentInjector: this.injector });
ref.setInput('message', message);
this.appRef.attachView(ref.hostView);
document.body.appendChild(ref.location.nativeElement);
setTimeout(() => {
this.appRef.detachView(ref.hostView);
ref.destroy();
}, duration);
}
}
// toast.component.ts
@Component({
selector: 'app-toast',
template: `<div class="toast">{{ message() }}</div>`,
styles: `.toast { position: fixed; bottom: 24px; right: 24px; background: #333; color: white; padding: 12px 18px; border-radius: 6px; }`,
})
export class ToastComponent {
message = input.required<string>();
}
// Anywhere in the app:
this.toastService.show('Saved successfully!');
Three files, one method, and you have a service that pops a styled toast from anywhere in the app — no template wiring required. The pattern scales up to modals, sidebars, command palettes, and any other always-on-top UI.
get_best_practicesFor most apps, use Angular Material’s MatDialog (or the CDK Overlay primitives that MatDialog wraps). It already handles focus trapping, ARIA roles, keyboard escape, click-outside, scroll lock, and overlay layering — all the accessibility and UX details that take days to reimplement. Roll your own with createComponent only if you need a very specific behavior the CDK doesn’t support, or you are building a UI library yourself. For the latter, the patterns in this lesson are the foundation.That closes Module 2. Module 3 (Signals & Reactivity) picks up next.
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.


