In Angular, how do a static class or style attribute, [class]/[style] bindings and [class.x]/[style.x.px] bindings on one element combine?
answer
- merged, not replaced
- single key beats map
- undefined defers, null removes
- compared by reference
- unit suffix in the name
basics
~20 sAngular 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 sAngular 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 linesimport { 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
Know the forms: [class.x] toggles one class, [class] and [style] take maps, and [style.width.px] appends a unit.
Explain merging, the single-key over map over static priority, undefined versus null, and why map values need new references.
Design components whose styling hooks compose with consumers' classes and host bindings, and debug why a style seems ignored.
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