skip to content

In Angular, when does NgClass or NgStyle still beat a plain [class] or [style] binding, and when should the binding win?

level: middleimportance: should knowfreq 55%

answer

  1. style guide prefers bindings
  2. === on objects
  3. space-separated keys
  4. mutated objects
  5. import and extra cost

basics

~20 s

Angular's style guide prefers [class] and [style] bindings: no import and less overhead. NgClass still wins when an object key holds several space-separated classes or when the bound object is mutated in place, because [class] compares objects with ===.

solid answer

~50 s

Since class and style bindings accept strings, arrays and objects, the Angular style guide recommends them over `NgClass` and `NgStyle`, which need an import and add a directive with its own per-check diffing. The binding compares a bound object or array with `===`, so you must pass a new reference to change it, and it does not accept several space-separated class names in one object key. `NgClass` does both: it walks the object on every check, so in-place mutations are picked up, and it splits keys like `'btn btn-primary'`. It also accepts a `Set`. `NgStyle` accepts unit-suffixed keys such as `'max-width.px'`; with bindings you write `[style.max-width.px]`. So keep `NgClass` for legacy mutable view models or multi-class keys, and use bindings, often with a `computed()` object, everywhere else. Neither directive is deprecated; `ng generate @angular/core:ngclass-to-class` migrates the safe cases.

code

ts · 12 lines
ts
import {Component, computed, signal} from '@angular/core';

@Component({
  selector: 'app-status-chip',
  template: `<span class="chip" [class]="chipClasses()" [style.max-width.px]="maxWidth()">{{ label() }}</span>`,
})
export class StatusChip {
  readonly label = signal('Paid');
  readonly urgent = signal(false);
  readonly maxWidth = signal(160);
  readonly chipClasses = computed(() => ({urgent: this.urgent(), paid: this.label() === 'Paid'}));
}

go deeper

for a junior

Know that [class.x], [class] and [style] cover most needs without imports, and that NgClass is the older directive form.

for a middle

Explain === reference checks versus NgClass's per-check walk, and the multi-class key and Set cases where NgClass still fits.

for a senior

Plan a migration: run the schematic for safe cases, find mutated class maps, and replace them with computed() objects.

for a principal

Weigh consistency and performance against churn: migrate hot paths first and leave stable legacy NgClass usage until touched.

## The two options **Class and style bindings** are built into template syntax: - `[class.active]="isActive()"` toggles one class; - `[class]="expr"` accepts a space-separated **string**, a **string array**, or an **object** of class names to truthy values; - `[style.width.px]="w()"` sets one property with a unit; - `[style]="expr"` accepts a **CSS declaration string** or an **object** of property names to values. **`NgClass` and `NgStyle`** are attribute directives from `@angular/common`, applied with `[ngClass]` and `[ngStyle]`. They predate the richer binding forms and are still public API, **not deprecated**. ## What the style guide says Angular's style guide says: **prefer `class` and `style` bindings over `NgClass` and `NgStyle`**. Its reasons: 1. the syntax is simpler and closer to plain HTML attributes; 2. the directives incur an **additional performance cost** compared with the built-in bindings; 3. bindings need no import, while a standalone component must import `NgClass` / `NgStyle` (or `CommonModule`). ## Where they differ in behaviour | Behaviour | `[class]` / `[style]` binding | `NgClass` / `NgStyle` | |---|---|---| | object or array change detection | compares the reference with `===`; you must pass a new object | walks the value on every check; detects in-place mutation | | several classes in one object key (`'btn btn-lg': cond`) | not supported | supported, the key is split on whitespace | | `Set` of class names | not a documented value type | supported by `NgClass` | | unit suffix | per property: `[style.max-width.px]` | inside the object: `{'max-width.px': w}` in `NgStyle` | | import needed | no | yes | | per-check cost | the binding's own update | an extra directive with its own diffing | The Angular bindings guide states the two main limits outright: class bindings do not support space-separated class names in a single key, and they do not see mutations of an object whose reference stays the same; **if you need either, use `NgClass`**. ## Choosing in practice Use the **binding** when: - the classes come from a signal or `computed()` that returns a **new object** when inputs change, the normal state in signal-based code; - you toggle a few individual classes with `[class.x]`; - you want no extra directive in hot lists. Keep **`NgClass`** when: - an existing view model **mutates** a class map in place (`this.classes.active = true`) and rewriting it is not worth it yet; - keys carry **several classes**, for example from a CSS framework's modifier combinations; - you pass a **`Set`** of classes. Keep **`NgStyle`** mainly when an object with unit-suffixed keys is produced elsewhere and passed through; otherwise split it into `[style.prop.unit]` bindings or a `computed()` style object. ## Migrating Angular ships schematics for both: `ng generate @angular/core:ngclass-to-class` and `ng generate @angular/core:ngstyle-to-style`. Both convert only usages they consider **safe**. The `NgStyle` migration skips bound object references unless `--best-effort-mode` is set, because a mutated object would stop updating after conversion; the `NgClass` migration skips keys with space-separated names unless `--migrate-space-separated-key` is set, in which case it emits one `[class.x]` binding per class. ## A migration bug to expect ```ts readonly classes = {active: false, disabled: false}; toggle(): void { this.classes.active = !this.classes.active; } ``` With `[ngClass]="classes"` this works. After switching to `[class]="classes"`, the toggle does nothing, because the object reference never changes. The fix is to produce a new object, ideally `readonly classes = computed(() => ({active: this.active(), disabled: this.disabled()}))`.

  • Why does switching an Angular [ngClass]="classes" to [class]="classes" sometimes break toggling?
    `NgClass` inspects the object's contents on every check, so mutating `classes.active` works. A `[class]` binding compares the object reference with `===`; if the component mutates the same object, the reference is unchanged and nothing updates. Produce a new object, for example from a `computed()`, or keep `NgClass` for that element.
  • Are Angular's NgClass and NgStyle deprecated?
    No. They are public API in `@angular/common` with no deprecation marker. The style guide prefers class and style bindings for simplicity and performance, and Angular ships schematics that convert the safe usages, but existing `NgClass` / `NgStyle` code keeps working and remains the right tool for multi-class keys or mutated objects.

saying these in an interview costs you the question

  • NgClass and NgStyle are deprecated and will be removed
  • [class] cannot take objects, so NgClass is needed for conditional classes
  • [class] picks up in-place mutations of the bound object
  • NgClass is faster than [class] because it batches DOM writes
  • Object keys with several space-separated classes work in [class]