Star5

Angular $implicit: Template Context and Structural Directives Explained

Understand Angular's $implicit template context. Pass data through ng-template, ngTemplateOutlet, and custom structural directives with real examples.

RJ
RAMA JONNADA
Senior Frontend Developer with 4+ years of experience in Angular and modern web technologies.

TL;DR: $implicit is a special key on the context object you pass to ngTemplateOutlet or return from a structural directive. It sets the default value that a template variable receives when you write let-item without an assignment — let-item is shorthand for let-item="$implicit". Use it to give the template caller a clean API without requiring them to know your internal context property names.

If you have ever used Angular's @for control flow and written let item of items, you used $implicit without knowing it. The item variable receives the value from $implicit — the current element — because Angular's @for block sets $implicit on each iteration's context. This guide explains the mechanism completely, from ng-template basics through custom structural directives, with modern Angular syntax throughout.


The Problem: Sharing Data with a Template Slot

Angular's component model is composable — a parent can pass <ng-template> blocks into a child component, and the child renders them in its own layout. But the child often needs to pass data back into that template block. A list component needs to tell the row template which item it is rendering. A modal needs to tell its content template whether it is open or closed.

The mechanism for this is the template context — an object the child attaches to the template outlet. $implicit is the conventional default slot in that context object.


ngTemplateOutlet and Context Basics

ngTemplateOutlet renders an ng-template reference. Its ngTemplateOutletContext input takes an object that becomes available as template variables inside the block.

import { Component, ChangeDetectionStrategy } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';

@Component({
  selector: 'app-demo',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [NgTemplateOutlet],
  template: `
    <ng-template #myTemplate let-message>
      <p>{{ message }}</p>
    </ng-template>

    <ng-container
      [ngTemplateOutlet]="myTemplate"
      [ngTemplateOutletContext]="{ $implicit: 'Hello from context!' }"
    />
  `,
})
export class DemoComponent {}

let-message with no right-hand side is shorthand for let-message="$implicit". Angular looks up $implicit in the context object and assigns it to the template variable message. The rendered output is <p>Hello from context!</p>.

You can also name the variable explicitly, which is useful when the context has multiple properties:

template: `
  <ng-template #myTemplate let-msg="$implicit" let-author="author">
    <p>{{ msg }} — by {{ author }}</p>
  </ng-template>

  <ng-container
    [ngTemplateOutlet]="myTemplate"
    [ngTemplateOutletContext]="{ $implicit: 'Great post', author: 'Alice' }"
  />
`

Here let-msg="$implicit" explicitly reads $implicit, and let-author="author" reads a named property. Both forms work together in the same template.


Real Pattern: Component That Accepts a Row Template

The most common real-world use of $implicit is a list or table component that lets its consumer customize how each item renders, while the component handles fetching and layout.

import {
  Component,
  ChangeDetectionStrategy,
  ContentChild,
  TemplateRef,
  input,
} from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';

interface ListContext<T> {
  $implicit: T;
  index: number;
  first: boolean;
  last: boolean;
}

@Component({
  selector: 'app-data-list',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [NgTemplateOutlet],
  template: `
    <ul>
      @for (item of items(); track $index; let i = $index, f = $first, l = $last) {
        <li>
          @if (rowTemplate) {
            <ng-container
              [ngTemplateOutlet]="rowTemplate"
              [ngTemplateOutletContext]="{ $implicit: item, index: i, first: f, last: l }"
            />
          } @else {
            {{ item | json }}
          }
        </li>
      }
    </ul>
  `,
})
export class DataListComponent<T> {
  readonly items = input.required<T[]>();

  @ContentChild(TemplateRef) rowTemplate?: TemplateRef<ListContext<T>>;
}

The consumer uses it like this:

<app-data-list [items]="products()">
  <ng-template let-product let-i="index" let-last="last">
    <span>{{ i + 1 }}. {{ product.name }}</span>
    @if (!last) { <hr /> }
  </ng-template>
</app-data-list>

let-product (no assignment) receives $implicit — the current item. let-i="index" and let-last="last" read named properties from the context. The consumer gets a clean API: just declare what you want with let-.


Custom Structural Directives and $implicit

Structural directives use the same context mechanism. When Angular expands *myDirective="expr" syntax, it creates an ng-template under the hood and your directive controls when and how it is instantiated.

Here is a practical example: a directive that renders its template only when the user has a given permission, and exposes the permission object to the template.

import {
  Directive,
  input,
  TemplateRef,
  ViewContainerRef,
  inject,
  effect,
} from '@angular/core';
import { AuthService } from './auth.service';

interface Permission {
  name: string;
  grantedAt: Date;
}

// Context type — used with the static ngTemplateContextGuard for type safety
export interface IfHasPermissionContext {
  $implicit: Permission;
  appIfHasPermission: Permission; // matches the selector, enables microsyntax typing
}

@Directive({ selector: '[appIfHasPermission]', standalone: true })
export class IfHasPermissionDirective {
  private readonly templateRef = inject(TemplateRef<IfHasPermissionContext>);
  private readonly vcr = inject(ViewContainerRef);
  private readonly auth = inject(AuthService);

  readonly appIfHasPermission = input.required<string>();

  constructor() {
    effect(() => {
      const permName = this.appIfHasPermission();
      const permission = this.auth.getPermission(permName);
      this.vcr.clear();

      if (permission) {
        this.vcr.createEmbeddedView(this.templateRef, {
          $implicit: permission,
          appIfHasPermission: permission,
        });
      }
    });
  }

  // Enables TypeScript to type-check template variables
  static ngTemplateContextGuard(
    _dir: IfHasPermissionDirective,
    ctx: unknown
  ): ctx is IfHasPermissionContext {
    return true;
  }
}

Usage in a template:

<!-- Microsyntax — let-perm receives $implicit (the Permission object) -->
<div *appIfHasPermission="'admin'; let perm">
  Admin access granted on {{ perm.grantedAt | date }}
</div>

<!-- Explicit ng-template form — identical result -->
<ng-template appIfHasPermission="'admin'" let-perm>
  Admin access granted on {{ perm.grantedAt | date }}
</ng-template>

The ngTemplateContextGuard static method is optional but important — it tells TypeScript what type let-perm is, enabling autocomplete and type errors in the IDE. Without it, perm is typed as any. The Angular structural directives guide on angular.dev covers ngTemplateContextGuard in detail.


$implicit vs Named Context Properties: When to Use Each

Use $implicit for the primary data of the template slot — the one thing the template caller cares about most. Named properties carry supplementary context the template might optionally read.

Slot $implicit Named properties
List row The item (Product, User, etc.) index, first, last, odd, even
Permission gate The Permission object grantedAt, scope
Async loader slot The loaded data loading, error
Dialog content The dialog control ref title, size

If your context has only one value, put it in $implicit and skip named properties. If it has many values, still put the most important one in $implicit — it gives the caller a friction-free default.


Using ZyraUI Components with Template Slots

ZyraUI uses this exact pattern for its composable components. For example, zyra-table accepts a cell template per column, passing the row item through $implicit:

import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { ZyraTableComponent } from '@zyra-ui/angular';

interface User {
  id: number;
  name: string;
  role: string;
}

@Component({
  selector: 'app-users-table',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [ZyraTableComponent],
  template: `
    <zyra-table [rows]="users()" [columns]="columns">
      <!-- Role cell with custom badge rendering -->
      <ng-template #roleCell let-user>
        <span [class]="'badge badge--' + user.role">{{ user.role }}</span>
      </ng-template>
    </zyra-table>
  `,
})
export class UsersTableComponent {
  readonly users = signal<User[]>([
    { id: 1, name: 'Alice', role: 'admin' },
    { id: 2, name: 'Bob', role: 'viewer' },
  ]);

  readonly columns = [
    { key: 'name', label: 'Name' },
    { key: 'role', label: 'Role', templateRef: 'roleCell' },
  ];
}

let-user in the cell template receives the full row object via $implicit, so you have the complete row context available for custom rendering — badges, action buttons, avatars, whatever the design requires. Explore all 60+ free components at zyraui.dev, including tables, cards, and more.


Type-Safe Context with ngTemplateContextGuard

Without ngTemplateContextGuard, template variables from let- bindings are typed as any in the IDE. Adding the static guard method to your directive or component fixes this:

// In your component or directive class:
static ngTemplateContextGuard<T>(
  _dir: DataListComponent<T>,
  ctx: unknown
): ctx is ListContext<T> {
  return true;
}

TypeScript then knows that let-product in <ng-template let-product> is typed as T (or whatever your $implicit type is), so you get autocomplete and catch type errors at build time rather than at runtime. This is documented in the Angular structural directives guide.


Wrapping Up

$implicit is the default slot in Angular's template context system. It makes let-variable bindings feel natural for the consumer — no need to know internal property names. Use it for the primary data in any template slot, name your directive's microsyntax binding the same as the selector to enable type narrowing via ngTemplateContextGuard, and expose supplementary values as named context properties. Once you internalize this pattern, composable Angular components and structural directives become straightforward to build and a pleasure to use. See the ZyraUI docs for real components that apply this pattern in production.


Frequently asked questions

What is $implicit in Angular?

$implicit is a reserved key on the context object passed to ngTemplateOutlet or returned from a structural directive. When a template uses let-variable without an explicit assignment (no ="someKey"), Angular reads the value from $implicit in the context. It is the default slot — the most important value the template slot provides to the caller.

What is the difference between let-item and let-item="$implicit"?

They are identical. let-item is shorthand for let-item="$implicit". Angular expands let-item into a lookup of the $implicit property on the context object. Writing let-item="$implicit" is more explicit but produces the same result. You typically see let-item in simple cases and let-item="$implicit" when the template also reads other named properties and you want to be consistent.

How do I pass multiple values through ng-template context?

Put the primary value in $implicit and additional values as named properties on the same context object. For example: { $implicit: item, index: i, first: isFirst }. In the template, let-item gets $implicit, let-i="index" gets the index, and let-f="first" gets the boolean.

How do I get type safety for $implicit template variables?

Add a static ngTemplateContextGuard method to your directive or component class. The method should return a type predicate (ctx is YourContextType) that tells TypeScript the shape of the context object. Angular's language service uses this to type-check let- bindings in the IDE and during compilation, so template variable types are inferred correctly instead of falling back to any.

Topics
angulartemplatesng-templatestructural directivesangular advanced