Angular 22 Selectorless Components: Import Classes Directly into Templates [2026]

Link copied
Angular 22 Selectorless Components: Import Classes Directly into Templates [2026]

Angular 22 Selectorless Components: Import Classes Directly into Templates [2026]

Angular 22 introduces selectorless components — a new authoring style that drops the selector string and the imports array, letting you reference a component class directly in another template. It is the framework's biggest template-layer change since @if @for block syntax replaced structural directives, and it is opt-in: existing selector-based components continue to work unchanged.

This is lesson 2.3 of the Angular Tutorial. It builds on lesson 2.1 (component anatomy) and lesson 2.2 (template syntax). The v22 feature is currently experimental — you can use it today by enabling a flag, and it will become the default authoring style in a future release.

The selector indirection problem #

Classic Angular ties a component class to a template tag via a string selector:

// child.component.ts
@Component({
  selector: 'app-child',
  template: `<p>Hello!</p>`,
})
export class ChildComponent {}

// parent.component.ts
@Component({
  selector: 'app-parent',
  imports: [ChildComponent],          // ← required: tell the template compiler about ChildComponent
  template: `<app-child />`,           // ← reference by selector string
})
export class ParentComponent {}

Three places repeat the same information:

  1. selector: 'app-child' — the string the template uses
  2. imports: [ChildComponent] — the class reference for the compiler
  3. <app-child /> — the string in the parent template

The selector string is the indirection — a runtime name that the compiler matches against imports. It is what makes Angular components feel different from regular TypeScript classes, and it is what selectorless components remove.

The selectorless shape #

The v22 syntax drops the selector and imports:

// child.component.ts
@Component({
  template: `<p>Hello!</p>`,           // no selector
})
export class Child {}                   // class name IS the tag name

// parent.component.ts
import { Child } from './child.component';

@Component({
  template: `<Child />`,                // reference the class directly
})
export class Parent {}                   // no imports array needed

Four things change:

  1. No selector field. The component is anonymous to Angular's selector matcher.
  2. No imports array. The compiler reads your TypeScript imports.
  3. The class name is the tag name. Capitalized — <Child /> not <child />.
  4. Standard TypeScript import. import { Child } from './child.component'.

This is identical to how JSX components work in React, Solid, and Preact: "the class is the tag." Angular is the last major framework to land this.

Why it matters #

Three concrete benefits:

Benefit Impact
No imports boilerplate A standalone component with five child components currently has a five-line imports array. Selectorless drops it entirely.
Real go-to-definition IDE click-through on <Child /> lands in the class file. Selector-based <app-child /> requires the IDE's Angular language service to resolve.
Tree-shaking is fully type-driven Imports the bundler sees are exactly the imports the template uses. No "this component is in imports: but unused in the template, the compiler doesn't know" cases.
Cleaner ecosystem semantics Library authors export classes; consumers import them and use them. Same model as every other modern framework.

The v17 standalone API moved Angular halfway to this. Selectorless is the second half.

Inputs and outputs work identically #

The selectorless syntax does not change how input() and output() work:

import { Component, input, output } from '@angular/core';

@Component({
  template: `
    <button (click)="clicked.emit()">{{ label() }} ({{ count() }})</button>
  `,
})
export class Counter {
  label = input.required<string>();
  count = input(0);
  clicked = output<void>();
}

import { Counter } from './counter';

@Component({
  template: `
    <Counter label="Total" [count]="current()" (clicked)="onClick()" />
  `,
})
export class Parent { /* ... */ }

Attribute syntax (label="Total"), property binding ([count]="current()"), event binding ((clicked)="onClick()") — all unchanged. Only the tag name and import mechanism differ.

Coexisting with selector-based components #

A single project can mix both styles. You will likely do so during migration:

// Legacy: still uses selector
@Component({
  selector: 'app-old-card',
  imports: [NewBadge],                   // can import selectorless components from a legacy one
  template: `<NewBadge text="NEW" />`,
})
export class OldCardComponent {}

// Selectorless: no selector
import { OldCardComponent } from './old-card';

@Component({
  template: `<OldCardComponent />`,      // imports a selector-based component by class
})
export class NewPage {}

The matrix:

Consumer Imports Can use
Selectorless component TypeScript import Selectorless components by class name; selector-based components by class name OR selector
Selector-based component imports: [...] array Either flavor — class name OR selector

The selector-based syntax is not deprecated, even in v22. Migrate components when convenient.

Enabling selectorless #

As of v22, selectorless is experimental — flip a single flag in tsconfig.app.json:

{
  "angularCompilerOptions": {
    "_unknownElementChecks": "emit",
    "unstableImportsAsTemplateRefs": true
  }
}

The exact flag name will stabilize in a later release. Once enabled, the template compiler accepts <ClassName /> for any imported class without a selector.

The Angular CLI will eventually flip this default, at which point selector-based code keeps working unchanged but new code defaults to selectorless. The migration path is forward-compatible — no breakage expected.

What this means for libraries #

Library authors should still export their components with selectors for the foreseeable future. Selector-based consumption works in every Angular version since v2; selectorless consumption requires v22+. Until the ecosystem widely adopts v22, selectors stay.

New internal-to-your-app components are the obvious candidates for selectorless — no public API contract, total v22 control.

Directives in selectorless code #

Directives (lesson 2.9) still need selectors — they attach to existing elements:

// Directive: needs a selector
@Directive({
  selector: '[hoverGlow]',
})
export class HoverGlow {}

// Selectorless component that uses the directive
import { HoverGlow } from './hover-glow';

@Component({
  imports: [HoverGlow],                  // directives need imports for now
  template: `<button hoverGlow>Click</button>`,
})
export class Card { /* ... */ }

The imports array survives for directives. The selectorless-by-class approach makes sense for components (where the class IS the element), less so for directives (which attach to whatever element they target).

Comparison table — what changes #

Aspect Selector-based (v2–v22) Selectorless (v22+)
Component declaration selector: 'app-child' Omit selector
Template tag <app-child /> <Child /> (class name)
imports array Required Not required for selectorless components
Directives Use imports array Still use imports array
Pipes Use imports array Use imports array
Bindings ([x], (y)) Identical Identical
Type-checking Selector-string lookup Real TypeScript class reference
Bundler tree-shaking Depends on imports analysis Depends on TypeScript imports

The model converges on standard TypeScript: import a class, use it. The selector indirection only stays alive for directives, where it earns its keep.

When to use which (during v22 migration) #

A practical recommendation:

  • Existing components — leave them on selectors. The migration is mechanical; do it when convenient (a future CLI schematic will handle it).
  • New internal components — use selectorless from day one.
  • Components you ship as a library — selectors only, for the next year. Cross-version compatibility matters.
  • Web Components (Angular Elements) — must use selectors (they become custom element names).

Common gotchas #

Symptom Cause Fix
<Child /> is not recognized in the template Class is not imported in TypeScript Add import { Child } from './child' at the top of the file
<Child /> lowercase fails Element-name detection treats lowercase as native HTML Capitalize class names (already the TypeScript convention)
imports: [Child] causes a duplicate You also referenced it by class name — fine Drop the imports entry; the class reference is enough
Directive selectorless tag does nothing Directives still need selector AND imports Add both back
<my-old-button /> from a library doesn't work The library exports with a selector; you must keep imports for it Use the legacy syntax for library components until they ship selectorless

What's next #

The second half of Module 2 covers content projection (2.6), lifecycle hooks (2.7), pipes (2.8), directives (2.9), <ng-template> and <ng-container> (2.10), styling (2.11), host metadata (2.12), signal queries (2.13), and programmatic component creation (2.14). By the end of Module 2 you can write components in every shape Angular supports, selectorless or not.

Module 3 then dives deep into the signal system.

Try it yourself #

A tiny app entirely in selectorless style:

// counter.ts
import { Component, signal } from '@angular/core';

@Component({
  template: `<button (click)="count.update(n => n+1)">Count: {{ count() }}</button>`,
})
export class Counter {
  count = signal(0);
}

// app.ts
import { Component } from '@angular/core';
import { Counter } from './counter';
import { bootstrapApplication } from '@angular/platform-browser';

@Component({
  template: `
    <h1>Selectorless demo</h1>
    <Counter />
    <Counter />
    <Counter />
  `,
})
export class App {}

bootstrapApplication(App);

Three counters, three independent instances. No selector, no imports. Run with the v22 flag enabled in tsconfig.app.json and watch each counter tick independently.

YouIf I migrate to selectorless, will my existing tests break?
Claude · used get_best_practicesNo — your tests use TestBed.createComponent(Counter) with the class reference, not the selector string. The runtime API is unchanged. The only place a selector string appears is in templates that other components use to reference yours; tests instantiate components by class. If you have integration tests that query by tag name (By.css('app-counter')), those would need a tweak after migration since the rendered tag name changes to <counter> (lowercased) — but you can use By.directive(Counter) to remain selector-agnostic.

Lesson 2.4 already covered input signals; the rest of Module 2 picks up content projection (2.6), lifecycle (2.7), and the templating primitives in 2.8–2.14.

Up next in Angular

More from this topic

View all Angular articles →

Enjoyed this article?

Get new Angular tutorials delivered. No spam — just code-first articles when they ship.

Leave a Comment

Your email stays private. Required fields are marked *

Leave a Comment

Your email stays private. Required fields are marked *