skip to content

In Angular, how do you give an <ng-content> slot fallback content, and when exactly does the fallback render instead of projected nodes?

level: middleimportance: nice to knowfreq 26%

answer

  1. children inside ng-content itself
  2. added in version 18
  3. only when the slot got no nodes
  4. a false @if still counts

basics

~20 s

Write the default markup inside the <ng-content> element itself, supported since Angular 18. It renders only when no node was assigned to that slot when the content was created; a projected @if block counts as assigned even while false.

solid answer

~40 s

Since Angular 18 you can put children inside `<ng-content>`, for example `<ng-content select="[card-header]">Untitled</ng-content>`. Angular renders those children only when the slot received **no projected nodes**. The check happens when the component's view is created, against the nodes distributed to that slot, not against what is visible later. So if a consumer writes `@if (title()) { <h2 card-header>{{ title() }}</h2> }`, the block itself is assigned to the header slot, and the fallback does not render even while the condition is false. To make a default depend on runtime data, compute it in the consumer or pass the value or a template as an input instead. Before Angular 18, components simulated fallbacks with a content query or CSS such as `:empty`.

go deeper

for a junior

Remember that markup inside ng-content is a default shown when nothing is projected into that slot.

for a middle

Explain that the decision is made at creation from assigned nodes, so a false @if block in the slot still suppresses the fallback.

for a senior

Choose inputs or templates over fallback content when a default must follow runtime data, and document the difference for consumers.

for a principal

Set library guidance on when defaults belong in projection fallbacks versus inputs, so behaviour stays predictable for consumers.

## Declaring a fallback Fallback content is ordinary markup written between the tags of an `<ng-content>` placeholder in the component's template: ```ts import {Component} from '@angular/core'; @Component({ selector: 'app-card', template: ` <header><ng-content select="[card-header]">Untitled</ng-content></header> <section><ng-content>Nothing to show yet.</ng-content></section> <footer><ng-content select="[card-footer]"><small>No actions</small></ng-content></footer> `, }) export class Card {} ``` Used as `<app-card><p>Hello</p></app-card>`, the body shows the paragraph, while the header shows `Untitled` and the footer shows `No actions`, because nothing was projected into them. The fallback can contain elements, bindings and components from the card's own template, since it belongs to the card's view. This feature arrived in **Angular 18**. Older code approximated it with a content query and an `@if` in the template, or with CSS on an empty wrapper. ## When it renders: the exact rule In Angular 22.2, when the runtime creates a projection placeholder it checks whether any node was distributed to that slot. If none was, it renders the fallback; otherwise it renders the projected nodes. Three consequences follow: 1. **It is decided at creation.** The check is part of creating the component's view, not something re-evaluated on every change-detection pass. 2. **It is about assigned nodes, not visible content.** A control-flow block in the consumer's content is itself a node. `@if (x) { <h2 card-header>..</h2> }` is assigned to the header slot whether `x` is true or false, so the slot is not empty and the fallback never appears. 3. **Whitespace usually does not count.** Angular removes whitespace-only text nodes by default (`preserveWhitespaces` is off), so indentation around content does not make the default slot non-empty. With whitespace preservation enabled, that can change. ## A worked comparison | Consumer content | Header slot receives | Header shows | |---|---|---| | nothing marked `card-header` | no nodes | fallback `Untitled` | | `<h2 card-header>Q3</h2>` | the `h2` | `Q3` | | `@if (t()) { <h2 card-header>{{ t() }}</h2> }`, `t()` empty | the `@if` block | nothing | | `<h2 card-header></h2>` (empty element) | the `h2` | an empty heading | The third and fourth rows are where teams get surprised: the fallback is not a "show this when the slot looks empty" feature. ## Getting a runtime default instead When a default must depend on data that changes, choose a mechanism evaluated at runtime: - **Let the consumer decide**: `@if (t()) { .. } @else { <h2 card-header>Untitled</h2> }` keeps one node in the slot either way. - **Pass data, not markup**: a `title = input<string>()` with `{{ title() ?? 'Untitled' }}` in the card. - **Pass a template**: accept a `TemplateRef` input and render either it or the card's own default. ## Fallbacks and components created from code When a component is created programmatically with projectable nodes, fallback content follows the same rule: a slot that is given nodes shows them, and one that is given none shows its fallback. The 19.0 changelog notes a related behaviour change: passing an empty array for a slot now renders that slot's fallback, and passing an empty text node suppresses it. Creating components from code is a topic of its own; the rule to remember is that an empty slot means no nodes assigned. ## Testing the defaults Because the rule is structural, it is easy to pin down in a component test. Render the card three ways: with no header content, with a static marked heading, and with a marked heading inside an `@if` whose condition is false. Assert that only the first shows the fallback. A test like this documents the behaviour for the next developer, who might otherwise assume that hiding the heading brings the default back, and it catches a refactor that swaps a fallback for an input or the other way round without the consumers' markup being updated. ## Why it is designed this way Projection is resolved structurally: the compiler knows the consumer's direct children and the component's slots, and the runtime distributes nodes once. Re-deciding fallback visibility on each check would mean tracking the rendered state of every projected block. The result is cheap and predictable, at the cost of the corner cases above.

  • Can the fallback markup use the card's own signals and components?
    Yes. Fallback content is part of the card's template, so it binds against the card's class and can use anything the card imports. It is projected content, not the consumer's markup, that binds against the consumer.
  • How would you show a default title that also appears when the consumer's title becomes empty at runtime?
    Not with `ng-content` fallback, which is decided when content is created. Either the consumer renders an `@else` branch that keeps a node in the slot, or the card takes the title as an input and falls back in its own template with `title() ?? 'Untitled'`.

saying these in an interview costs you the question

  • Fallback content reappears whenever the projected @if turns false
  • Fallback content binds against the consumer's component
  • ng-content fallback has existed since the first Angular versions
  • An empty projected element triggers the fallback
  • Fallback rendering is re-evaluated on every change detection pass