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.
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.
<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.
<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:
<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
- Is
@forinternally equivalent tong-containerwith*ngForregarding DOM generation and whitespace nodes? - Is the whitespace behavior with
@forandpreserveWhitespaces: trueintentional? - What is the recommended way to avoid these whitespace nodes when using
@for, without disablingpreserveWhitespacesglobally 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)
- Is the whitespace behavior intentional? preserveWhitespaces explained
- How to avoid whitespace nodes when using @for (recommended fixes)
- Practical examples and a migration checklist
- Sources
- Conclusion
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-containerhas 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@forsyntax is compiled to anng-container+*ngForunder the hood, and produces the same whitespace text nodes whenpreserveWhitespaces: trueis 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
preserveWhitespaceskeeps 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
@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
preserveWhitespacesdefault tofalse), 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.
- Local override: use ngPreserveWhitespaces=“false” for the specific subtree
- Wrap the
@forblock (or its parent container) and setngPreserveWhitespaces="false"so only that subtree ignores literal template whitespace:
<!-- 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.)
- 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:
<!-- 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.
- Change the layout so inter-element text nodes don’t affect rendering (recommended for visual spacing)
- Instead of relying on
display:inline-blockwhich is sensitive to whitespace text nodes, switch to CSS layouts that ignore in-between text (flexbox/grid):
.container { display: flex; gap: 0.5rem; /* or use column-gap / row-gap */ }
.inlineBlock { display: block; /* or whatever styling you need */ }
<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.
- Revert that particular bit to element-level structural directive (if acceptable)
- If you prefer the old behavior and want minimal changes, keep
*ngForon 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
@forblock.
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+gapfor 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
<!-- 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:
.list { display: flex; gap: 8px; align-items: center; }
<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
- Stack Overflow — @for adds whitespace nodes when preserveWhitespaces: true — is it equivalent to ng-container?
- Angular — Whitespace in templates
- GitHub — Issue #56323: control flow is adding unnecessary whitespace
- GitHub — Issue #21049: preserveWhitespaces:false is too aggressive and not context aware
- Angular — Advanced configuration (preserveWhitespaces in component metadata)
- Ninja Squad — What’s new in Angular 4.4? (historical context for whitespace handling)
- GitHub — commit f1a0632: set preserveWhitespaces to false by default
- Angular — Control flow guide
- Medium — Angular Control Flow vs Structural Directives (migration commentary)
- DEV.to — Lessons learned from Angular’s control flow migration script
- GitHub — Issue #54133: preserveWhitespaces attribute doesn’t work
- ANGULARarchitects — Clever White Space Handling
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.