Skip to main content
The @angular/cdk/portal package provides a way to render dynamic content in a target location. Portals are used extensively by the Overlay system.

Installation

npm install @angular/cdk
import {PortalModule} from '@angular/cdk/portal';

Types of Portals

ComponentPortal

Render a component dynamically:
import {Component, inject, ViewContainerRef} from '@angular/core';
import {ComponentPortal} from '@angular/cdk/portal';

@Component({selector: 'app-dynamic', template: '<p>Dynamic Component!</p>'})
export class DynamicComponent {}

@Component({
  selector: 'app-portal-example',
  template: `
    <button (click)="loadComponent()">Load Component</button>
    <ng-template [cdkPortalOutlet]="selectedPortal"></ng-template>
  `,
})
export class PortalExample {
  selectedPortal: ComponentPortal<DynamicComponent>;

  loadComponent() {
    this.selectedPortal = new ComponentPortal(DynamicComponent);
  }
}

TemplatePortal

Render a template dynamically:
import {Component, TemplateRef, ViewChild, ViewContainerRef, inject} from '@angular/core';
import {TemplatePortal} from '@angular/cdk/portal';

@Component({
  selector: 'app-template-portal',
  template: `
    <button (click)="showTemplate()">Show Template</button>
    
    <ng-template #myTemplate let-data>
      <div class="template-content">
        <h3>{{ data.title }}</h3>
        <p>{{ data.message }}</p>
      </div>
    </ng-template>
    
    <div class="outlet">
      <ng-template [cdkPortalOutlet]="selectedPortal"></ng-template>
    </div>
  `,
})
export class TemplatePortalExample {
  @ViewChild('myTemplate') template: TemplateRef<any>;
  private viewContainerRef = inject(ViewContainerRef);
  selectedPortal: TemplatePortal<any>;

  showTemplate() {
    this.selectedPortal = new TemplatePortal(
      this.template,
      this.viewContainerRef,
      {title: 'Hello', message: 'Template Portal'}
    );
  }
}

DomPortal

Move existing DOM elements:
import {Component, ElementRef, ViewChild} from '@angular/core';
import {DomPortal} from '@angular/cdk/portal';

@Component({
  selector: 'app-dom-portal',
  template: `
    <div #content class="moveable">
      This content can be moved!
    </div>
    
    <button (click)="moveContent()">Move Content</button>
    
    <div class="target">
      <ng-template [cdkPortalOutlet]="domPortal"></ng-template>
    </div>
  `,
})
export class DomPortalExample {
  @ViewChild('content') content: ElementRef;
  domPortal: DomPortal;

  moveContent() {
    this.domPortal = new DomPortal(this.content);
  }
}

Portal Outlets

cdkPortalOutlet Directive

<ng-template [cdkPortalOutlet]="portal"></ng-template>

Programmatic Portal Attachment

import {Component, ViewChild} from '@angular/core';
import {CdkPortalOutlet, ComponentPortal} from '@angular/cdk/portal';

@Component({
  selector: 'app-programmatic',
  template: `<ng-template cdkPortalOutlet></ng-template>`,
})
export class ProgrammaticExample {
  @ViewChild(CdkPortalOutlet) portalOutlet: CdkPortalOutlet;

  ngAfterViewInit() {
    const portal = new ComponentPortal(DynamicComponent);
    const componentRef = this.portalOutlet.attach(portal);
    
    // Access component instance
    componentRef.instance.someProperty = 'value';
  }
}

Advanced Examples

Portal with Injector

import {Component, inject, Injector} from '@angular/core';
import {ComponentPortal} from '@angular/cdk/portal';

@Component({
  selector: 'app-injector-portal',
  template: `<ng-template [cdkPortalOutlet]="portal"></ng-template>`,
})
export class InjectorPortalExample {
  private injector = inject(Injector);
  portal: ComponentPortal<DynamicComponent>;

  createPortalWithDependencies() {
    // Create custom injector
    const customInjector = Injector.create({
      parent: this.injector,
      providers: [
        {provide: 'CUSTOM_DATA', useValue: {id: 123}}
      ]
    });

    this.portal = new ComponentPortal(
      DynamicComponent,
      null,  // viewContainerRef
      customInjector
    );
  }
}

Conditional Portal Rendering

import {Component} from '@angular/core';
import {ComponentPortal, TemplatePortal} from '@angular/cdk/portal';

@Component({
  selector: 'app-conditional-portal',
  template: `
    <button (click)="currentPortal = componentPortal">Show Component</button>
    <button (click)="currentPortal = templatePortal">Show Template</button>
    <button (click)="currentPortal = null">Clear</button>
    
    <ng-template #myTemplate>
      <p>Template content</p>
    </ng-template>
    
    <div class="portal-container">
      <ng-template [cdkPortalOutlet]="currentPortal"></ng-template>
    </div>
  `,
})
export class ConditionalPortal {
  componentPortal = new ComponentPortal(DynamicComponent);
  templatePortal: TemplatePortal<any>;
  currentPortal: ComponentPortal<any> | TemplatePortal<any> | null;

  @ViewChild('myTemplate') template: TemplateRef<any>;
  private viewContainerRef = inject(ViewContainerRef);

  ngAfterViewInit() {
    this.templatePortal = new TemplatePortal(this.template, this.viewContainerRef);
  }
}

Portal with Context

import {Component, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core';
import {TemplatePortal} from '@angular/cdk/portal';

interface TemplateContext {
  $implicit: string;
  count: number;
}

@Component({
  selector: 'app-context-portal',
  template: `
    <ng-template #contextTemplate let-item let-count="count">
      <div>Item: {{ item }}, Count: {{ count }}</div>
    </ng-template>
    
    <ng-template [cdkPortalOutlet]="portal"></ng-template>
  `,
})
export class ContextPortal {
  @ViewChild('contextTemplate') template: TemplateRef<TemplateContext>;
  private viewContainerRef = inject(ViewContainerRef);
  portal: TemplatePortal<TemplateContext>;

  ngAfterViewInit() {
    this.portal = new TemplatePortal<TemplateContext>(
      this.template,
      this.viewContainerRef,
      {
        $implicit: 'Primary Value',
        count: 42
      }
    );
  }
}

API Reference

ComponentPortal

const portal = new ComponentPortal(
  component: ComponentType<T>,
  viewContainerRef?: ViewContainerRef | null,
  injector?: Injector | null,
  componentFactoryResolver?: ComponentFactoryResolver | null,
  projectableNodes?: Node[][] | null
);

TemplatePortal

const portal = new TemplatePortal(
  template: TemplateRef<C>,
  viewContainerRef: ViewContainerRef,
  context?: C
);

DomPortal

const portal = new DomPortal(element: ElementRef<HTMLElement> | HTMLElement);

Portal Methods

MethodReturnsDescription
attach(host)ComponentRef | EmbeddedViewRef | nullAttach portal to outlet
detach()voidDetach from outlet
isAttachedbooleanCheck if attached

CdkPortalOutlet

Selector: [cdkPortalOutlet], [portalOutlet]
InputTypeDescription
cdkPortalOutletPortal<any>Portal to attach
MethodReturnsDescription
attach(portal)ComponentRef | EmbeddedViewRefAttach portal
detach()voidDetach portal
hasAttached()booleanCheck if has portal

Use Cases

Dynamic Tab Content

@Component({
  selector: 'app-tabs',
  template: `
    <div class="tabs">
      <button *ngFor="let tab of tabs" (click)="selectTab(tab)">
        {{ tab.label }}
      </button>
    </div>
    <div class="tab-content">
      <ng-template [cdkPortalOutlet]="selectedPortal"></ng-template>
    </div>
  `,
})
export class TabsComponent {
  tabs = [
    {label: 'Tab 1', component: Tab1Component},
    {label: 'Tab 2', component: Tab2Component},
  ];
  selectedPortal: ComponentPortal<any>;

  selectTab(tab: any) {
    this.selectedPortal = new ComponentPortal(tab.component);
  }
}

Reusable Modal Service

import {Injectable, Injector} from '@angular/core';
import {Overlay, OverlayRef} from '@angular/cdk/overlay';
import {ComponentPortal} from '@angular/cdk/portal';

@Injectable({providedIn: 'root'})
export class ModalService {
  private overlay = inject(Overlay);
  private injector = inject(Injector);

  open<T>(component: ComponentType<T>, data?: any): OverlayRef {
    const overlayRef = this.overlay.create({
      hasBackdrop: true,
      positionStrategy: this.overlay.position().global().centerHorizontally().centerVertically(),
    });

    const injector = Injector.create({
      parent: this.injector,
      providers: [{provide: 'MODAL_DATA', useValue: data}]
    });

    const portal = new ComponentPortal(component, null, injector);
    overlayRef.attach(portal);

    return overlayRef;
  }
}

Best Practices

  1. Clean up - Detach portals when no longer needed
  2. Use typed contexts - For TemplatePortal type safety
  3. Provide injectors - For dependency injection in dynamic components
  4. Reuse portals - Detach/reattach instead of recreating
  5. Handle lifecycle - Components in portals have normal lifecycle

See Also

Build docs developers (and LLMs) love