What does Angular's self-closing-tags migration change in templates, and why does it leave an empty <div></div> alone?
answer
- cosmetic, not behavioural
- only non-standard tags
- empty or whitespace only
- HTML rules for native elements
basics
~10 sAngular's self-closing-tags migration rewrites empty component and custom-element tags such as <app-spinner></app-spinner> into <app-spinner />. It skips standard HTML elements like <div>, which cannot self-close in HTML, and any tag with real content.
solid answer
~30 s`ng generate @angular/core:self-closing-tag` converts templates to Angular's self-closing syntax, supported since v16. It collects every element that is not already self-closing, is **not a known HTML element**, and has **no content or only whitespace**, and rewrites `<app-spinner></app-spinner>` to `<app-spinner />`. A `<div></div>` is left alone because it is a standard HTML element, and in HTML a non-void element cannot be self-closed. Elements with text or child nodes are skipped because self-closing would drop their content. It is purely cosmetic: the compiled output is the same, so it is safe to run last, after the structural migrations.
code
html · 11 lines<!-- Before -->
<app-spinner></app-spinner>
<app-avatar [user]="user"> </app-avatar>
<div class="divider"></div>
<app-card>Totals</app-card>
<!-- After ng generate @angular/core:self-closing-tag -->
<app-spinner />
<app-avatar [user]="user" />
<div class="divider"></div>
<app-card>Totals</app-card>go deeper
Know what the migration does: empty component tags become self-closing, standard HTML elements stay as they are.
Explain the three conditions an element must meet and why native elements like div are excluded.
Place it at the end of a modernisation plan, as its own reviewable commit, and keep the convention for new templates.
Judge whether a repository-wide cosmetic diff is worth the review and merge-conflict cost for teams working in parallel.
Angular templates accept **self-closing tags** for components since v16: `<app-spinner />` instead of `<app-spinner></app-spinner>`. The **self-closing-tags migration** converts existing templates to that shorter form. ## Running it ```shell ng generate @angular/core:self-closing-tag ``` The schematic's full name is `self-closing-tags-migration`, and it takes a `path` option to limit it to part of the project. It is optional; `ng update` does not run it for you. ## Which elements it converts The migration walks every template and picks an element only if **all** of these hold: 1. it is **not already self-closing**; 2. its tag name is **not a known standard HTML element**, so in practice it is a component selector or a custom element; 3. it has **no children, or a single child that is only whitespace**. Each match is rewritten from `<tag ...></tag>` to `<tag ... />`, keeping attributes and bindings. ## What it leaves alone, and why | Template | Converted? | Reason | | :-- | :-- | :-- | | `<app-spinner></app-spinner>` | Yes | Component tag, no content | | `<app-avatar [user]="u"> </app-avatar>` | Yes | Only whitespace inside | | `<div></div>` | No | Standard HTML element | | `<span class="dot"></span>` | No | Standard HTML element | | `<app-card>Title</app-card>` | No | Has content | | `<app-card><h2>Title</h2></app-card>` | No | Has child elements | The `<div>` case is the one interviewers like. In HTML, only **void elements** such as `<img>` or `<input>` have no closing tag, and `<div />` is not a valid way to write an empty `div`. Angular's own template parser agrees: self-closing a standard non-void element is a template error, *Only void, custom and foreign elements can be self closed*. So the migration only rewrites component and custom-element tags, which Angular does allow to self-close, and templates keep compiling. Elements with content are skipped for the obvious reason: self-closing them would delete the content projected into them. ## Why it is safe - **No behaviour change.** Self-closing and explicit closing tags compile to the same thing for the elements it touches. - **No TypeScript changes.** Only template text is rewritten, in inline and external templates. - **Easy to review.** Every change is a one-line tag rewrite. ## Where it fits in a modernisation plan - Run it **last**, after structural migrations such as control flow, standalone and the signal migrations, so the cosmetic diff does not hide real changes. - Commit it **on its own**, ideally together with a formatter run, so reviewers can skim it. - Remember that code generated after it will not be affected; many teams add a lint rule or style guide note so new templates keep the convention. ## Where it sits among the template migrations The self-closing-tags migration is one of several template-only schematics in `@angular/core`, all optional: - `control-flow` rewrites structural directives into blocks; - `ngclass-to-class` and `ngstyle-to-style` replace `NgClass` and `NgStyle` with plain class and style bindings where possible; - `self-closing-tag` shortens empty component tags. Of these, it is the only one that changes nothing but syntax, which is why it is the natural last step. ## Common misunderstandings - It does not add `/>` to every empty element in the template, only to non-standard ones. - It does not affect component behaviour, change detection or content projection. - It is not required for anything: templates with explicit closing tags remain valid in current Angular.
- Does Angular's self-closing-tags migration change how a component renders?No. It only rewrites template text for empty component and custom-element tags, and the self-closing form compiles to the same output as an explicit closing tag. That is why it is safe to run as a separate, cosmetic commit after the structural migrations.
saying these in an interview costs you the question
- The migration turns every empty element, including <div></div>, into a self-closing tag.
- Self-closing a component tag changes how its content projection works.
- It also collapses component tags that contain text or child elements.
- Templates must be migrated because closing tags are deprecated in current Angular.