Web

Angular @for Whitespace Nodes with preserveWhitespaces

Why Angular @for adds whitespace text nodes like ng-container *ngFor when preserveWhitespaces: true? Understand the behavior, equivalence to *ngFor, and fixes like ngPreserveWhitespaces or CSS flex without global changes.

1 answer 1 view

Angular @for with preserveWhitespaces: true adds whitespace nodes like ng-container *ngFor?

I’m testing Angular’s new control flow syntax @for with preserveWhitespaces: true and observing different whitespace behavior compared to *ngFor.

Environment

  • Angular: 17+ (control flow syntax enabled)
  • preserveWhitespaces: true

Case 1: ng-container + *ngFor

Using *ngFor on an ng-container generates whitespace text nodes between elements in Firefox DevTools.

html
<div>
 <ng-container *ngFor="let item of items; let i = index">
 <div class="inlineBlock">{{ item }}</div>
 </ng-container>
</div>

Case 2: *ngFor directly on element

No whitespace nodes are generated.

html
<div>
 <div *ngFor="let item of items; let i = index" class="inlineBlock">
 {{ item }}
 </div>
</div>

Case 3: Auto-migrated to @for

Angular’s migration converts Case 2 to:

html
<div>
 @for (item of items; track item; let i = $index) {
 <div class="inlineBlock">{{ item }}</div>
 }
</div>

This behaves like Case 1, adding whitespace text nodes between elements.

Questions

  1. Is @for internally equivalent to ng-container with *ngFor regarding DOM generation and whitespace nodes?
  2. Is the whitespace behavior with @for and preserveWhitespaces: true intentional?
  3. What is the recommended way to avoid these whitespace nodes when using @for, without disabling preserveWhitespaces globally or per component?

Yes—when you use Angular @for with preserveWhitespaces: true the control‑flow block is compiled to a container-like structure (effectively an ng-container/*ngFor under the hood), so the template’s literal whitespace is preserved as DOM text nodes between repeated elements. This is expected behavior when preserveWhitespaces is true. To avoid the extra text nodes without turning off preserveWhitespaces globally, either override whitespace locally with ngPreserveWhitespaces=“false”, eliminate the template whitespace around the repeated element, or change layout (use flex/grid or other CSS) so inter-element text nodes don’t affect visual spacing.


Contents


Why Angular @for adds whitespace nodes (ng-container equivalence)

Short answer: the new control‑flow syntax compiles into the same kind of DOM structure that a logical container (an ng-container) plus *ngFor would create, and that interacts with Angular’s whitespace rules.

  • The observable difference between your Case 1 (ng-container + *ngFor) and Case 2 (*ngFor on the element) comes from where the literal template whitespace sits. An ng-container has no rendered host element, so any spaces/newlines that are written around the repeated fragment end up as text nodes in the rendered DOM when whitespace preservation is enabled. The StackOverflow discussion that examined this migration notes exactly that: the @for syntax is compiled to an ng-container + *ngFor under the hood, and produces the same whitespace text nodes when preserveWhitespaces: true is set (StackOverflow thread).

  • Angular’s template compiler treats whitespace according to the component/global setting. The official guide explains that Angular normally collapses or removes superfluous whitespace unless you opt into preservation; enabling preserveWhitespaces keeps literal spaces and newlines from your template as DOM text nodes (Angular: Whitespace in templates).

What you’ll see in DevTools is the familiar “#text” nodes containing only spaces/newlines between your repeated

elements. That’s not a browser bug — it’s a direct result of the template AST + whitespace policy + how @for is lowered by the compiler.


Is the whitespace behavior intentional? preserveWhitespaces explained

Yes — this is intentional (or at least expected) behavior when you ask Angular to preserve whitespace.

  • Angular gives you control over whitespace semantics. If you set preserveWhitespaces: true (component or global level), Angular will keep the template’s literal whitespace; if you don’t, Angular will remove/collapse those whitespace nodes to avoid unnecessary DOM text nodes and improve rendering performance (Angular docs).

  • Historically Angular changed defaults around this setting (see the commit that set preserveWhitespaces default to false), and the team has discussed edge cases and trade-offs in GitHub issues — for example some users reported regressions when migrating to control flow syntax because whitespace semantics changed or became more visible (commit f1a0632, issue: #56323). Those discussions confirm the behavior is tied to intentional whitespace semantics, not a random bug in the control-flow syntax.

So: if preserveWhitespaces is true, the compiler follows the template’s whitespace. @for was designed to keep the same whitespace semantics as the structural directives it replaces, hence the parity with an ng-container + *ngFor.


How to avoid whitespace nodes when using @for (recommended fixes)

You don’t have to disable preserveWhitespaces for the whole component or app to fix this. Pick the option that fits your codebase and readability preferences.

  1. Local override: use ngPreserveWhitespaces=“false” for the specific subtree
  • Wrap the @for block (or its parent container) and set ngPreserveWhitespaces="false" so only that subtree ignores literal template whitespace:
html
<!-- Only this block will have whitespace removed -->
<div ngPreserveWhitespaces="false">
 @for (let item of items; track item; let i = $index) {
 <div class="inlineBlock">{{ item }}</div>
 }
</div>

This is the cleanest, least invasive fix when you must keep preserveWhitespaces: true elsewhere. (Community discussion and issues point to ngPreserveWhitespaces as the local control mechanism; see GitHub issue #54133 and the docs linked above.)

  1. Remove the template whitespace around the repeated element
  • If you remove the literal newline/space characters that sit between the repeated element instances, nothing will be preserved:
html
<!-- compact form: no spaces/newlines between generated items -->
<div>
 @for (let item of items; track item; let i = $index) {<div class="inlineBlock">{{ item }}</div>}
</div>

It’s a bit uglier to read, but it works because there are no literal whitespace characters left for Angular to preserve.

  1. Change the layout so inter-element text nodes don’t affect rendering (recommended for visual spacing)
  • Instead of relying on display:inline-block which is sensitive to whitespace text nodes, switch to CSS layouts that ignore in-between text (flexbox/grid):
css
.container { display: flex; gap: 0.5rem; /* or use column-gap / row-gap */ }
.inlineBlock { display: block; /* or whatever styling you need */ }
html
<div class="container">
 @for (let item of items; track item; let i = $index) {
 <div class="inlineBlock">{{ item }}</div>
 }
</div>

Flexbox/Grid + gap is the most robust approach: spacing is explicit and you avoid fiddling with template whitespace.

  1. Revert that particular bit to element-level structural directive (if acceptable)
  • If you prefer the old behavior and want minimal changes, keep *ngFor on the element for those specific cases instead of using @for. The migration script can be manually adjusted — but that means leaving mixed syntax in your app.

Which option to pick?

  • For readability + minimal changes: ngPreserveWhitespaces="false" on the wrapper.
  • For long-term layout stability and future-proof CSS: convert layout to flex/grid and use gap.
  • For tiny patches or quick fixes: remove the newline/space in the template where you have the @for block.

Practical examples and a migration checklist

Quick migration checklist you can run through after automatic conversion to @for:

  • Scan for whitespace‑sensitive constructs: inline-block elements, text nodes relied on for content spacing, or places where you saw differences in DevTools.
  • Prefer local fixes: add ngPreserveWhitespaces="false" around the control‑flow block when you want to keep global/component whitespace behavior unchanged.
  • Consider layout rewrites: convert inline-block patterns to display:flex + gap for predictable spacing.
  • Test in your target browsers (Chrome, Firefox, Safari) and in assistive technologies if textual whitespace matters for accessibility.

Example: fix for your Case 3 with local override

html
<!-- original (migrated to @for) -->
<div>
 @for (let item of items; track item; let i = $index) {
 <div class="inlineBlock">{{ item }}</div>
 }
</div>

<!-- fix: local override to remove whitespace for only this block -->
<div ngPreserveWhitespaces="false">
 @for (let item of items; track item; let i = $index) {
 <div class="inlineBlock">{{ item }}</div>
 }
</div>

If you prefer not to change attributes, change the layout:

css
.list { display: flex; gap: 8px; align-items: center; }
html
<div class="list">
 @for (let item of items; track item; let i = $index) {
 <div>{{ item }}</div>
 }
</div>

Finally, remember: some fixes affect semantics (text nodes can matter in preformatted text or accessible names). Don’t remove whitespace blindly if the space is meaningful for content.


Sources


Conclusion

Yes — Angular’s @for behaves like a container-backed *ngFor with respect to whitespace, so with preserveWhitespaces: true you will see extra DOM text nodes. That is expected: you told the compiler to preserve template whitespace. The simplest, least-disruptive fixes are to use ngPreserveWhitespaces="false" around the specific @for block, remove the template whitespace around the repeated element, or switch the layout to flex/grid so in-between text nodes don’t matter. Pick the approach that balances readability and maintainability for your codebase, and test the changes in the contexts where whitespace actually affects rendering or accessibility.

Authors
Verified by moderation
Moderation
Angular @for Whitespace Nodes with preserveWhitespaces