A component is a TypeScript class with a decorator on top of it. The decorator gives the class a template and a name that other templates can call it by. In modern Angular that is genuinely all there is to it — there is no NgModule to declare it in and no registration step to forget.
import { Component } from '@angular/core';
@Component({
selector: 'app-greeting',
template: `<h1>Hello</h1>`,
})
export class Greeting {}Once that exists, any template that imports it can use its selector as if it were an HTML tag:
<app-greeting />That is the whole idea. HTML gives you a fixed vocabulary of elements; components let you add your own, and yours can take inputs, hold state, and contain other components.
A real one
Here is a complete component from the pizza app, copied as it is written. It shows a loading spinner, and it is about as small as a useful component gets:
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
@Component({
selector: 'app-spinner',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="text-center" [class.py-5]="!inline()">
<div class="spinner-border text-danger" role="status">
<span class="visually-hidden">{{ label() }}</span>
</div>
</div>
`,
})
export class Spinner {
readonly label = input('Loading…');
readonly inline = input(false);
}Two lines of class body, and both of them are inputs. Most components really do look like this: a template, and a handful of fields the template reads.
The decorator, field by field
selector
The name other templates use. app-spinner becomes
<app-spinner />. The app- prefix is not required by the framework, but
it is the convention and it matters more than it looks: it keeps your components from ever colliding
with a real HTML element or with a third-party library's. The prefix is set once in
angular.json and the CLI applies it to everything it generates.
template or templateUrl
Exactly one of them. template holds the markup inline as a backtick string;
templateUrl points at a separate .html file. Neither is more correct. The
pizza app uses inline templates for small components like the one above and separate files for pages
like the menu, on the rough rule that once you are scrolling past the template to reach the class,
it belongs in its own file.
⚠️ An inline template is a JavaScript template literal, so a backtick inside it ends it — including a backtick in a comment. The error you get points somewhere else entirely. If a component suddenly will not compile after you wrote a comment, that is the reason.
imports
What this component's template is allowed to use. Another component, a directive, a pipe — if the template mentions it, it goes here:
@Component({
selector: 'app-product-card',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [MoneyPipe],
template: `
...
<span class="fw-bold">
from <span class="text-pizza-red">{{ cheapest() | money }}</span>
</span>
...
`,
})The template uses the money pipe, so MoneyPipe is in
imports. Leave it out and the build fails with a message naming the pipe, which is the
best kind of error: it tells you exactly what to add and where.
This array is the same mental model as an import statement in a React file. The
difference is that a TypeScript import at the top of the file satisfies the type checker,
while this array is what satisfies the template compiler. You need both, and forgetting the
second one is the mistake everybody makes for about a week.
changeDetection
Every component in the pizza app sets
ChangeDetectionStrategy.OnPush. It tells Angular to re-check this component only when one
of its inputs changes or a signal it read changes, rather than on every pass. It has a lesson of its
own later in this track; for now, treat it as the habit worth having and copy it.
styles or styleUrls
Optional, and absent from both components above because the pizza app styles with Bootstrap classes and a global Sass theme. When you do use them, the styles are scoped to the component by default — Angular rewrites them so they cannot leak out. That has a lesson of its own too.
Standalone, and the NgModule you no longer need
If you meet an older Angular codebase, or an older tutorial, you will see every component
listed in an @NgModule before it could be used:
// The old way. You do not need this.
@NgModule({
declarations: [Spinner, ProductCard, MenuPage],
imports: [CommonModule],
exports: [Spinner],
})
export class SharedModule {}Components are standalone by default from Angular 19 onwards, and there is no
NgModule anywhere in the pizza app. A component declares what it needs in its own
imports array and that is the end of it. If you are reading something that talks about
declarations, it predates this and you can skip that part.
How a component ends up on the page
Three steps, and they are worth following once so the rest stops feeling like magic.
The page has one element in its body:
<body>
<app-root></app-root>
</body>main.ts boots the application into it:
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';
bootstrapApplication(App, appConfig)
.catch((err) => console.error(err));And App — the component whose selector is app-root — is the shell
everything else hangs off:
@Component({
selector: 'app-root',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RouterOutlet, AppNavbar, AppFooter, CartDrawer, ToastHost],
template: `
<div class="d-flex flex-column min-vh-100">
<app-navbar (openCart)="cartOpen.set(true)" />
<main class="flex-grow-1">
<router-outlet />
</main>
<app-cart-drawer [open]="cartOpen()" (closed)="cartOpen.set(false)" />
<app-footer />
<app-toast-host />
</div>
`,
})
export class App {
readonly cartOpen = signal(false);
}Every component in the app is somewhere below that template, either written into it directly
or dropped in by <router-outlet /> when the URL changes.
Generating one
You rarely type the boilerplate. The CLI writes it:
ng generate component shared/spinner
# the short form everyone actually uses
ng g c shared/spinnerThat creates the class, applies your configured selector prefix, and — depending on your
angular.json — either an inline template or a separate .html file.
Naming
The convention in the pizza app, and the one the CLI follows, is a kebab-case file name and a
PascalCase class: product-card.ts exports ProductCard, whose selector is
app-product-card. Three spellings of the same name, one for each context. It looks
redundant written out like that, and in practice you stop noticing within a day.
What is next
Everything above is the container. The next lesson is what goes inside it: interpolation, property binding, event binding, and the rules Angular enforces on the expressions you write in a template.