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:
selector: 'app-child'— the string the template usesimports: [ChildComponent]— the class reference for the compiler<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:
- No
selectorfield. The component is anonymous to Angular's selector matcher. - No
importsarray. The compiler reads your TypeScript imports. - The class name is the tag name. Capitalized —
<Child />not<child />. - 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.
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
Enjoyed this article?
Get new Angular tutorials delivered. No spam — just code-first articles when they ship.


