Angular Component Anatomy: @Component, Template, Selector, and Standalone Imports [2026]

Link copied
Angular Component Anatomy: @Component, Template, Selector, and Standalone Imports [2026]

Angular Component Anatomy: @Component, Template, Selector, and Standalone Imports [2026]

Almost every screen of every Angular app is built from one shape: a TypeScript class with an @Component decorator on top. Open any Angular codebase and the same five-or-six fields appear over and over — selector, template, styleUrl, imports, plus the class body underneath. This lesson dismantles that shape into its parts.

This is lesson 2.1 of the Angular Tutorial, opening Module 2. Module 1 covered everything around components — CLI, bootstrap, DI, TypeScript, the build. From here on we are writing application code, and every lesson in Module 2 builds on the anatomy below.

The minimum viable component #

The smallest legal Angular component is three lines plus a class:

import { Component } from '@angular/core';

@Component({
  selector: 'app-hello',
  template: '<h1>Hello!</h1>',
})
export class HelloComponent {}

Three pieces are at work:

Piece What it does
@Component({...}) A decorator that registers metadata on the class — Angular reads this at compile time
selector: 'app-hello' The custom-element name Angular wires this component to. Templates that say <app-hello> get this rendered
template: '<h1>Hello!</h1>' The HTML Angular replaces the selector with at runtime

That is sufficient for Angular to render <app-hello></app-hello> anywhere. Every other field you will see in real code is optional — they exist to add styling, imports, lifecycle, change detection, animations, or providers.

The full @Component metadata table #

Reading any production component, you will encounter these fields:

Field Type What it does
selector string CSS-style selector used to recognize the component in templates
template / templateUrl string Inline template or path to an external .html file
styles / styleUrl / styleUrls string / string / string[] Inline CSS or path(s) to external stylesheet(s)
imports array Other standalone components, directives, pipes used in this template
providers array DI providers scoped to this component's element injector
viewProviders array Like providers, but invisible to content projected via <ng-content>
changeDetection enum Default (legacy) or OnPush (modern default in v22+)
encapsulation enum Emulated (default), ShadowDom, or None — controlled in lesson 2.11
animations array Angular animations triggers (Module 13)
host object Bindings applied to the host element itself (lesson 2.13)
standalone boolean Default true since v19. Almost never set explicitly.

Most components use 4-6 of these. The CLI's ng g component scaffolds with selector, imports, templateUrl, and styleUrl. Everything else is opt-in.

Selector flavors #

The selector field is a CSS-style selector — not arbitrary text. Three shapes work:

@Component({ selector: 'app-header' })           // matches <app-header>
@Component({ selector: '[appTooltip]' })         // matches any element with appTooltip attribute
@Component({ selector: '.app-glow' })            // matches any element with class app-glow

The element selector (app-header) is the common case. The attribute and class selectors are for directives, not components — they let you decorate existing elements without wrapping them. Lesson 2.9 covers directives.

Conventionally, every component selector is prefixed (Angular Material uses mat-, the CLI defaults to app- for new projects). This prevents collisions with native HTML elements and with components from other libraries.

The imports array — the standalone era #

Since standalone became the default (v19), every component declares its own dependencies. If your template uses <router-outlet>, you import RouterOutlet. If it uses the async pipe, you import AsyncPipe.

import { Component } from '@angular/core';
import { RouterOutlet, RouterLink } from '@angular/router';
import { UserCardComponent } from './user-card.component';
import { CurrencyPipe } from '@angular/common';

@Component({
  selector: 'app-shell',
  imports: [RouterOutlet, RouterLink, UserCardComponent, CurrencyPipe],
  template: `
    <a routerLink="/users">Users</a>
    <router-outlet></router-outlet>
    <app-user-card [user]="current()" />
    <p>{{ price | currency:'USD' }}</p>
  `,
})
export class AppShell { /* ... */ }

The imports array is the only way to make external symbols available inside this component's template. If you forget to add an import, the template compiler refuses the build with a useful error pointing at the offending symbol.

New-in-v22: the @Component decorator's selectorless syntax (lesson 2.3) drops the imports requirement when components are imported directly as classes in the template — but for now, treat imports as required for anything non-trivial.

Templates: inline vs external #

Two flavors, both legal:

// Inline (good for tiny templates)
@Component({
  template: `<h1>{{ title }}</h1>`,
})

// External file (good for everything else)
@Component({
  templateUrl: './user-list.html',
})

The choice is purely ergonomic. Inline templates work well for single-line components. Past ~10 lines, external files give you better editor support — most IDEs treat .html files as a full HTML language service with Angular template extensions enabled. The CLI's ng g component defaults to external.

You cannot have both. Setting template AND templateUrl on the same component fails at compile time.

Styles: inline vs external #

Mirror story:

@Component({
  styles: `
    h1 { color: var(--accent); }
    .glass { backdrop-filter: blur(20px); }
  `,
})

// OR

@Component({
  styleUrl: './user-list.css',
})

The one detail that catches people: styles are scoped by default. The h1 { color: red } rule above applies only to <h1> elements inside this component's template — not to every <h1> on the page. Angular implements this via a CSS attribute selector added at compile time (lesson 2.11 walks ViewEncapsulation in depth).

If you want a global rule, write it in src/styles.css (the global stylesheet) instead, or set encapsulation: ViewEncapsulation.None on the component.

The class body — properties, methods, signals #

The class is where your state and behavior live. A typical 2026 component:

import { Component, signal, computed, inject } from '@angular/core';
import { AuthService } from '../auth.service';

@Component({
  selector: 'app-greeting',
  template: `
    @if (user(); as u) {
      <p>Hi, {{ u.name }}! You've logged in {{ visits() }} times.</p>
    } @else {
      <p>Please sign in.</p>
    }
  `,
})
export class GreetingComponent {
  private auth = inject(AuthService);

  user = this.auth.user;                            // exposed signal
  visits = signal(0);
  isReturning = computed(() => this.visits() > 1);  // derived signal

  increment() {
    this.visits.update(n => n + 1);
  }
}

The class members fall into four buckets:

Bucket Examples Purpose
Injected dependencies auth = inject(AuthService) Services this component uses
Signals visits = signal(0) Reactive state the template reads
Computed signals isReturning = computed(...) Derived reactive state
Methods increment(), onSubmit() Behaviors triggered by user actions or lifecycle

Not every component has all four. A presentational component may have only injected signals and computed derivations; an action-heavy form component may have many methods.

What's NOT in a modern component #

A few things you will see in older code that have either gone away or moved:

  • @Input() and @Output() decorators — replaced by the input() and output() functions called as field initializers (lessons 2.4 and 2.5).
  • @ViewChild() and @ContentChild() decorators — replaced by viewChild() and contentChild() signal queries (lesson 2.13).
  • Lifecycle interfaces (OnInit, OnDestroy) — still supported but mostly replaced by effect(), afterNextRender(), and DestroyRef.onDestroy() (lesson 2.7).
  • changeDetection: ChangeDetectionStrategy.OnPush — still valid, but redundant in v22+ where OnPush is the default for new components.
  • standalone: true — also redundant, default since v19. Setting it explicitly is a no-op.

If you see any of these in older blog posts or Stack Overflow answers, recognize them as historical artifacts — your new code should not need them.

Common gotchas #

Symptom Cause Fix
Can't bind to 'X' since it isn't a known property of 'app-Y' The child component doesn't have an input() named X, or its input() is misspelled Check the child component's field definitions
'app-foo' is not a known element The parent component's imports array doesn't include FooComponent Add it to the imports array
Styles bleed out of the component The selector started with :host or used ::ng-deep (which is global) Drop ::ng-deep, use a CSS variable on the host element instead
Template variable typo not caught strictTemplates is off Turn strictTemplates on in tsconfig.json (lesson 1.4)
Two components with same selector A new component reused an existing selector Selectors must be unique in the imported set — pick a different prefix or suffix

What's next #

Lesson 2.2 walks the template syntax in depth — every binding form ({{ }}, [], (), [()]), the @if @for @switch @let block syntax, two-way binding desugaring, expression syntax, and the gotchas around safe-navigation and async pipes. Lesson 2.3 follows with selectorless components — the v22 syntax that lets you import a class directly into a template without an imports array entry.

Lessons 2.4 and 2.5 cover the input/output APIs that components use to communicate. Lesson 2.6 handles content projection (<ng-content>). Lesson 2.7 walks lifecycle hooks. By the end of Module 2 you will have written components in every shape Angular supports.

Try it yourself #

The shortest demo of every essential piece in one block:

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

@Component({
  selector: 'app-counter',
  template: `
    <button (click)="inc()">{{ label() }}: {{ count() }}</button>
  `,
  styles: `button { font-size: 1.2em; padding: 8px 14px; }`,
})
export class CounterComponent {
  label = input('Count');                  // 2.4 — input signal
  count = signal(0);                       // signal state
  changed = output<number>();              // 2.5 — output signal

  inc() {
    this.count.update(n => n + 1);
    this.changed.emit(this.count());
  }
}

Three fields (label, count, changed), one method (inc), one inline template, one inline style block — and the component is feature-complete.

YouWhen should I split a template into its own .html file vs keep it inline?
Claude · used get_best_practicesRough rule: under 10 lines stays inline, anything bigger goes external. Inline templates are easier to grep for and easier to refactor for tiny presentational components. External files give you a real HTML language service in your IDE, better diff readability in PRs, and a cleaner Lighthouse view of which component owns what markup. The CLI’s ng g component defaults to external because it’s the right answer for most components.

Lesson 2.2 picks up the template syntax — every binding form, every block, every gotcha.

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 *