A standalone component is not tied to any Angular module. Instead, it lists the components, directives, and pipes that its template relies on directly.

Declarable dependencies

A standalone Angular component uses the Component.imports metadata option to define its local component scope by declaring its dependents.

Take a hero detail component written as a standalone, inspired by the Tour of Heroes tutorial:

import { NgIf, UpperCasePipe } from '@angular/common';
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  imports: [FormsModule, NgIf, UpperCasePipe],
  selector: 'toh-hero-detail',
  standalone: true,
  styleUrls: ['./hero-detail.component.css'],
  template: `
    <div *ngIf="hero">
      <h2>{{ hero.name | uppercase }} Details</h2>
      <div><span>id: </span>{{ hero.id }}</div>
      <div>
        <label for="hero-name">Hero name: </label>
        <input
          id="hero-name"
          [(ngModel)]="hero.name"
          placeholder="Hero name"
        />
      </div>
      <button type="button" (click)="goBack()">go back</button>
      <button type="button" (click)="save()">save</button>
    </div>
  `,
})
export class HeroDetailComponent {
  // Class body omitted
}

Notice that the Component.standalone metadata option is explicitly enabled with true.

After that, we populate the Component.imports metadata option with the needed declarables or modules that export them. For example, the structural directive NgIf and the pipe UppercasePipe from @angular/common are both imported individually. This is feasible because these are both standalone in recent Angular releases.

ℹ️ Note
Alternatively, importing CommonModule is still valid to bring all standard Angular declarables into a standalone component's local scope.

Moreover, FormsModule is included in the Component.imports metadata option. This makes the attribute directive NgModel available within the hero detail component's local scope.

ℹ️ Note
All components, directives, and pipes referenced in Component.imports must have standalone: true. This is set via Component.standalone, Directive.standalone, and Pipe.standalone.

As of Angular 15, this metadata option defaults to standalone: false. This default may evolve based on community feedback and adoption.

Looking at the template, the NgIf directive appears in action:

<div *ngIf="hero">
  <!-- Conditional content omitted -->
</div>

We can also spot UppercasePipe being applied within the template:

<h2>{{ hero.name | uppercase }} Details</h2>

Lastly, two-way binding connects the Hero#name property to a form control via the NgModel directive:

<input
  id="hero-name"
  [(ngModel)]="hero.name"
  placeholder="Hero name"
/>

Local component scope

The dependency graph for this standalone hero detail component reveals how it connects to its declarable dependencies:

The local component scope of the standalone hero detail component

Apart from the FormsModule, the graph aligns closely with the template's actual indirect needs:

The declarables used in the hero detail component's template

Indirect dependencies means the template uses element selectors, attribute selectors, and pipe names coming from the Component.selector, Directive.selector, and Pipe.name metadata options of the declarable dependencies.

Transitive compilation scope

Now compare this with the transitive compilation scope for a traditional, module-based component. The classic style adds cognitive load:

The transitive compilation scope of the classic hero detail module (simplified)

Both HeroDetailModule and CommonModule add extra layers of indirection. In this classic setup, HeroDetailComponent has no direct link to NgIf or UppercasePipe. The dependency flow runs through HeroDetailModule, which references the component, rather than the component declaring its own needs. CommonModule adds yet another level of abstraction, wrapping two declarables we actually need in the template.

With the classic approach, the dependency graph does not mirror what the template actually uses.

Consider the complex logic required from the Angular compiler to resolve these relationships, and the maintenance burden when Angular, TypeScript, or other tooling undergoes major updates.

Furthermore, the classic graph shown above is oversimplified. A more complete view of the hero detail module's transitive compilation scope expands significantly:

The transitive compilation scope of the classic hero detail module (expanded)

That includes 52 declarables in the transitive scope, while the component template only uses 3. Fifty-two.

Fortunately, the Angular Development Kit's Build Optimizer removes unused declarables from both transitive and local scopes. Without this tree shaking, importing a module with extra exports would bloat the bundle every time.

This leads us to the next consideration: Maintenance.

Maintaining declarable dependencies

Revisit the expanded transitive compilation scope above. As the codebase grows, keeping declarables in sync with the compilation scope becomes challenging. How do you tell if a template still depends on any declarable from a large module?

This problem grows with each component in a module and with every imported non-SCAM module.

ℹ️ Note
Now is the time to move SCAMs to standalone declarables. That was the original intent all along.

With standalone components, it's straightforward to review the template and cross-check it against the imports listed in the Component.imports metadata option.

Wrapping Up

A standalone component’s local component scope is confined to the declarable dependencies it explicitly declares through the Component.imports metadata option.

Keeping this local scope consistent with what the component template actually uses is our responsibility. That means cross-checking the template against the standalone declarables and Angular modules registered in the Component.imports metadata option, and updating them together as needed.

Frequently used declarables—think NgIf or UppercasePipe—which were once only available via CommonModule exports, are now standalone themselves. They can be imported straight into the Component.imports metadata option without pulling in the whole module.