skip to content

In Angular, how do a static class or style attribute, [class]/[style] bindings and [class.x]/[style.x.px] bindings on one element combine?

level: middleimportance: should knowfreq 50%

answer

  1. merged, not replaced
  2. single key beats map
  3. undefined defers, null removes
  4. compared by reference
  5. unit suffix in the name

basics

~20 s

Angular merges them all onto the element. On a conflict a single-key binding like [style.width.px] beats the [style] map, which beats the static attribute; undefined defers to the lower-priority value. Map bindings compare by reference, so mutating an object changes nothing.

solid answer

~40 s

Angular treats classes and styles as mergeable, not as single properties. `class="list" [class]="listType()" [class.expanded]="isExpanded()"` renders all three sources together. `[class]` accepts a space-separated string, a string array or an object whose truthy values switch classes on; `[style]` accepts a CSS declaration string or an object. For the same class or style key, the more specific binding wins: `[style.width.px]` over a `width` in the `[style]` map, and both over the static `style` attribute. A binding that evaluates to `undefined` means "no opinion", so the next lower-priority value applies. Map bindings are compared with `===`, so you must pass a new object or array to trigger an update. The `.px` suffix appends a unit; any unit such as `em` or `rem` works the same way.

code

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

@Component({
  selector: 'app-listbox',
  template: `
    <ul
      class="list"
      [class]="listClasses()"
      [class.expanded]="expanded()"
      style="width: 300px"
      [style.width.px]="width()"
    >
      <li>...</li>
    </ul>
  `,
})
export class Listbox {
  protected readonly expanded = signal(true);
  protected readonly compact = signal(false);
  protected readonly width = signal<number | undefined>(undefined); // 300px applies
  protected readonly listClasses = computed(() => ({ box: true, compact: this.compact() }));
}

go deeper

for a junior

Know the forms: [class.x] toggles one class, [class] and [style] take maps, and [style.width.px] appends a unit.

for a middle

Explain merging, the single-key over map over static priority, undefined versus null, and why map values need new references.

for a senior

Design components whose styling hooks compose with consumers' classes and host bindings, and debug why a style seems ignored.

for a principal

Decide how far styling is driven from templates versus CSS and design tokens, keeping bindings for state rather than theming.

## Classes and styles are special bindings Most property bindings replace a value. `class` and `style` are different: an element's classes and inline styles usually come from several places at once - static markup, a whole-map binding, single-key toggles, and host bindings of directives. Angular's styling runtime **merges** these sources instead of letting the last one overwrite the rest. ## The binding forms | Form | Example | Accepts | |---|---|---| | static attribute | `class="list"`, `style="width: 300px"` | literal markup | | class map | `[class]="listClasses()"` | string of names, `string[]`, or `{ name: truthy }` object | | single class | `[class.expanded]="isExpanded()"` | truthy / falsy | | style map | `[style]="panelStyles()"` | declaration string or `{ property: value }` object | | single style | `[style.display]="shown() ? 'block' : 'none'"` | a value | | single style with unit | `[style.height.px]="height()"` | a number or string; the unit is appended | With `class="list" [class]="'box'" [class.expanded]="true"`, the element ends up with **all three** classes: `list box expanded`. Angular does not guarantee the order of class names, and it ignores string entries that are not valid class names. ## Who wins on a conflict When several sources set the **same** class or style property, Angular applies a priority order: 1. **Single-key bindings** (`[class.active]`, `[style.width.px]`) - highest. 2. **Map bindings** (`[class]`, `[style]`) for that key. 3. **Static** `class` / `style` attributes - lowest. (Bindings coming from directives' host bindings also take part, at a lower priority than the template's own bindings; the host-binding rules are a separate subject.) Two values are treated differently when a higher-priority binding has nothing to say: - **`undefined`** means "no opinion": Angular falls back to the next lower-priority value. `<div style="width: 300px" [style.width.px]="w()">` shows `300px` while `w()` is `undefined`. - **`null`** is an actual value that clears the property, so the lower-priority value is not consulted. ## Units For a single-style binding, a third segment is a **unit suffix**: `[style.width.px]="120"` writes `width: 120px`, and `[style.font-size.rem]="1.25"` writes `font-size: 1.25rem`. When the value is `null`, `undefined` or an empty string, no unit is appended, so the property is simply cleared. ## Reference equality for maps For `[class]` and `[style]` bound to an **object or array**, Angular checks the new value against the previous one with `===`. Consequences: - `this.classes.active = true` on the same object does **nothing**, because the reference did not change. - Return a new object instead, typically from a `computed()`: `classes = computed(() => ({ active: this.isActive(), compact: this.compact() }))`. ## Where NgClass and NgStyle still fit The `NgClass` and `NgStyle` directives from `@angular/common` predate the native bindings. The Angular docs point to `NgClass` for two things plain class bindings do not do: **space-separated class names in one object key** (`{ 'btn btn-primary': cond }`) and **picking up mutations** of the same object. For new code, the native `[class]` and `[style]` bindings are the default. ## Debugging a class or style that seems ignored - Look for a **higher-priority** source for the same key: a `[style.x]` binding hides the map's `x`, and a template binding hides a directive's host binding. - Check whether the value is `null` (clears the property) or `undefined` (hands control down) - they behave differently. - Check whether an object or array bound with `[class]` / `[style]` was **mutated in place** rather than replaced. - Check the unit: `[style.width]="120"` writes a unitless value that the browser rejects, while `[style.width.px]="120"` writes `120px`. - Remember stylesheet rules still apply: an inline style from a binding beats most stylesheet rules, but not `!important` declarations. ## Practical guidance - Use `[class.x]` for single toggles and `[class]` for a computed set of classes; mixing them with a static `class` is safe. - Prefer `undefined` when a single-key binding should hand control back to a map or static value, and `null` when it should clear the property. - Keep the maps in `computed()` signals so a new object is produced exactly when inputs change.

  • In <div style="width: 300px" [style.width.px]="w">, what is the width when w is undefined, and when it is null?
    With `undefined` the single-key binding has no opinion, so Angular falls back to the static `300px`. With `null` the binding supplies an explicit empty value, the property is cleared, and the static width is not consulted.
  • Why might [class]="classes" stop reflecting changes after a refactor to mutate the object in place?
    Angular compares map values with `===`. Mutating a property of the same object leaves the reference unchanged, so the binding looks unchanged and nothing is re-applied. Produce a new object on each change, for example from a `computed()`, or use `NgClass`, which the docs point to for mutation cases.

Coats of paint: the static attribute is the base coat, the [class] or [style] map goes over it, and single-key bindings are touch-ups on top. undefined leaves a patch unpainted so the coat underneath shows through; null scrapes that patch bare.

saying these in an interview costs you the question

  • [class] replaces the static class attribute entirely
  • The [style] map wins over [style.width.px] for width
  • Mutating the bound classes object updates the element
  • undefined and null clear a style in exactly the same way
  • [class] keys may contain several space-separated class names