The official style guide was rewritten in 2025 (with v20) and the CLI changed the default conventions: the files no longer have type suffixes (.component.ts, .service.ts), and the classes no longer end in Component. The idea: the name describes what the thing is, not what kind of Angular artefact it is.
| Modern (CLI v20+, current style guide) | Classic (with suffixes) | |
|---|---|---|
| File | user-profile-card.ts / .html / .scss | user-profile-card.component.ts |
| Class / function | UserProfileCard | UserProfileCardComponent |
| Selector / name | app-user-profile-card | app-user-profile-card |
| CLI command | ng generate component user-profile-card # ng g c … | |
Files
user-profile.ts ← the component (class UserProfile)
user-profile.html ← template
user-profile.scss ← styles
user-profile.spec.ts ← test
the same base name = easy to find, easy to move together
auth-store.ts ← service with state
users-api.ts ← service for data access
time-ago-pipe.ts ← pipe (class TimeAgoPipe)
auth-guard.ts ← functional guard (authGuard)Existing projects with suffixes can stay that way. Consistency is what matters. If you want to keep the suffixes for newly generated files:
// Do you prefer the old suffixes (.component.ts, .service.ts)? You can keep them in angular.json:
{
"schematics": {
"@schematics/angular:component": { "type": "component" },
"@schematics/angular:directive": { "type": "directive" },
"@schematics/angular:service": { "type": "service" },
"@schematics/angular:guard": { "typeSeparator": "." },
"@schematics/angular:pipe": { "typeSeparator": "." }
}
}
// What matters is being CONSISTENT across the whole project.The order of members in a component
@Component({
selector: 'app-user-profile',
imports: [Avatar, DatePipe],
templateUrl: './user-profile.html',
styleUrl: './user-profile.scss',
host: { '[class.compact]': 'compact()' },
})
export class UserProfile {
// 1. The component’s API: inputs, models, outputs (readonly)
readonly userId = input.required<number>();
readonly compact = input(false, { transform: booleanAttribute });
readonly edit = output<number>();
// 2. Queries (readonly)
private readonly nameInput = viewChild<ElementRef<HTMLInputElement>>('name');
// 3. Dependencies (private readonly)
private readonly users = inject(UsersStore);
// 4. State and derived values — protected if only the template uses them
protected readonly user = computed(() => this.users.byId(this.userId()));
protected readonly editing = signal(false);
// 5. Methods — named after WHAT THEY DO, not after the event that triggers them
protected startEditing(): void {
this.editing.set(true);
}
}Short rules
// ✓ inject() instead of the constructor
private readonly http = inject(HttpClient);
// ✓ protected for members used only in the template (not part of the public API)
protected readonly fullName = computed(() => `${this.first()} ${this.last()}`);
// ✓ readonly for everything Angular initialises: input, output, model, viewChild, signal
readonly value = model(0);
// ✓ host: {} instead of @HostBinding / @HostListener
host: { '[attr.aria-expanded]': 'open()', '(keydown.escape)': 'close()' }
// ✓ [class.x] / [style.x] instead of ngClass / ngStyle
// ✓ @if / @for / @switch instead of *ngIf / *ngFor / ngSwitch
// ✓ thin lifecycle hooks: call methods with clear names
ngOnInit() { this.loadInitialFilters(); }Naming handlers
<!-- ✗ the name describes the event -->
<button (click)="handleClick()">Save</button>
<input (keydown.enter)="onKeydown()" />
<!-- ✓ the name describes the action -->
<button (click)="saveProfile()">Save</button>
<input (keydown.enter)="search()" />Guide summary
| Rule | Why |
|---|---|
| One concept per file | Easy to find, test and move. |
| The same name for .ts/.html/.scss/.spec.ts | Visual grouping, quick search. |
| Organise by feature, not by type | What changes together stays together. |
A prefix on selectors (app-, adm-) | No collisions with native elements or library ones. |
inject() instead of the constructor | Readable, works with inheritance and functions. |
protected for template members | Separates the public API from display details. |
readonly for Angular properties | Prevents accidental overwriting of a signal. |
| Simple logic in the template | Calculations go into computed(). |
| Handlers named after the action | saveProfile() says what it does; onClick() doesn’t. |
How to group files into folders is covered in Folder structure; how to name types and models, in Interfaces & models.
Selectors and prefixes
Components have element selectors in kebab-case, with the project prefix (app-user-card). Directives have attribute selectors in camelCase, with the same prefix (appTooltip). Pipes have camelCase names without a prefix. The prefix is set once in angular.json, and the CLI uses it on every ng generate. The @angular-eslint/component-selector and directive-selector rules check them in lint.
@Component({ selector: 'app-user-card' }) // element, kebab-case, with prefix
@Directive({ selector: '[appTooltip]' }) // attribute, camelCase, with prefix
@Pipe({ name: 'relativeTime' }) // camelCase, no prefix
// angular.json → projects.<app>.prefix: "app"Automatic migrations to the modern style
You do not have to rewrite an old project by hand. Angular CLI has migrations that change the code for you, file by file. All of these exist in Angular 22.2:
ng generate @angular/core:standalone # NgModule → standalone
ng generate @angular/core:control-flow # *ngIf / *ngFor → @if / @for
ng generate @angular/core:inject # constructor → inject()
ng generate @angular/core:signal-inputs # @Input → input()
ng generate @angular/core:outputs # @Output → output()
ng generate @angular/core:signal-queries # @ViewChild & co. → viewChild()
ng generate @angular/core:signals # the three above, in one step
ng generate @angular/core:service # @Injectable → @Service (v22)
ng generate @angular/core:route-lazy-loading # component: → loadComponent
ng generate @angular/core:self-closing-tag # <app-x></app-x> → <app-x />
ng generate @angular/core:ngclass-to-class # [ngClass] → [class]
ng generate @angular/core:ngstyle-to-style # [ngStyle] → [style]
ng generate @angular/core:cleanup-unused-imports # unused imports in componentsRun them one at a time, on a clean branch, and check the diff and the tests after each one. Most accept --path so you can migrate a single folder, which means you can go gradually, feature by feature.
Common pitfalls
- Two styles mixed in the same folder (
user.component.tsnext touser-card.ts): worse than either one applied consistently. - One huge migration in a single PR: nobody can review it. Go folder by folder and keep the migration separate from logic changes.
- Generic names (
utils.ts,helpers.ts,common.service.ts) that end up collecting everything. A name should say what the file does.
Cheat sheet
user-card.ts / .html / .css / .spec.ts one concept per file, no type suffix
class UserCard no Component at the end
selector: app-user-card [appTooltip] prefix on selectors
inject() protected readonly in components
saveProfile() not onClick() handlers named after the action
ng generate @angular/core:<migration>