# Angular + BEM CSS Best Practices
**Version 1.0.0**
Community
January 2026
> **Note:**
> This document is for AI agents and LLMs to follow when writing, reviewing,
> or refactoring Angular component styles. Enforces BEM methodology with
> Angular's component architecture for CSS, SCSS, and SASS.
---
## Abstract
A methodology guide for combining Angular's component architecture with BEM (Block Element Modifier) CSS naming convention to create reusable components and enable code sharing in front-end development. Contains 6 rules with bad/good examples in CSS, SCSS, and SASS.
---
## When to Apply
Reference these guidelines when:
- Writing CSS/SCSS/SASS for Angular components
- Naming CSS classes in Angular templates
- Reviewing component styles for consistency
- Deciding whether to split a component based on styling complexity
- Setting up CSS architecture for a new Angular project
- Refactoring existing styles to follow BEM methodology
## Core Principles
- **1 Component = 1 BEM Block** — The block name matches the component selector
- **Max 2 Levels** — Only `Block` and `Block__Element`, never `Block__Element__SubElement`
- **Split When Deep** — If you need a third level, extract a child component
- **Flat Selectors** — No descendant, child, or tag-qualified selectors
- **Semantic Names** — Element names describe _what_, not _how_ or _where_
- **Modifiers for Variants** — Use `--modifier` for states and variants, always with the base class
## Rule Categories by Priority
| Priority | Rule | Impact | File |
|----------|------|--------|------|
| 1 | Block = Component Selector | CRITICAL | `bem-block-selector` |
| 2 | Max 2 Levels of Nesting | CRITICAL | `bem-max-nesting` |
| 3 | Split Child Components | CRITICAL | `bem-split-components` |
| 4 | Element Naming Conventions | HIGH | `bem-element-naming` |
| 5 | Modifier Patterns | HIGH | `bem-modifier-patterns` |
| 6 | No Cascading Selectors | HIGH | `bem-no-cascading` |
## Quick Reference
### 1. Block = Component Selector (CRITICAL)
- `bem-block-selector` - BEM block name must match the Angular component selector (minus prefix)
### 2. Maximum 2 Levels (CRITICAL)
- `bem-max-nesting` - Never nest beyond `.block__element` — no `.block__element__subelement`
### 3. Split Child Components (CRITICAL)
- `bem-split-components` - Extract child components when BEM depth would exceed 2 levels
### 4. Element Naming (HIGH)
- `bem-element-naming` - Use semantic, descriptive kebab-case names for BEM elements
### 5. Modifier Patterns (HIGH)
- `bem-modifier-patterns` - Use `--modifier` correctly for states and variants with Angular class bindings
### 6. No Cascading (HIGH)
- `bem-no-cascading` - Avoid descendant, child, and tag-qualified selectors — keep BEM flat
## BEM Cheat Sheet
```
.block → Component root (matches selector)
.block__element → Child part of the component
.block--modifier → Variant of the entire block
.block__element--modifier → Variant of a single element
✅ .user-card
✅ .user-card__avatar
✅ .user-card--featured
✅ .user-card__name--highlighted
❌ .user-card__header__title (3 levels)
❌ .user-card .user-card__avatar (descendant selector)
❌ div.user-card (tag-qualified)
```
---
## Rule 1: BEM Block Must Be the Component Selector
**Impact: CRITICAL** — Consistent naming eliminates selector conflicts and enables component-scoped styling
The BEM Block name must match the Angular component selector. This creates a 1:1 mapping between components and BEM blocks, eliminating naming conflicts and making the relationship between markup and styles immediately obvious.
**Incorrect (Block name differs from component selector):**
```typescript
// ❌ Component selector is 'app-user-profile' but BEM block is 'profile-card'
@Component({
selector: 'app-user-profile',
template: `
...
{{ name }}
`
})
export class UserProfileComponent {}
```
```css
/* ❌ CSS - Block name doesn't match selector */
.profile-card {
display: flex;
padding: 16px;
}
.profile-card__avatar {
width: 48px;
height: 48px;
}
.profile-card__name {
font-weight: bold;
}
```
```scss
// ❌ SCSS - Block name doesn't match selector
.profile-card {
display: flex;
padding: 16px;
&__avatar {
width: 48px;
height: 48px;
}
&__name {
font-weight: bold;
}
}
```
```sass
// ❌ SASS - Block name doesn't match selector
.profile-card
display: flex
padding: 16px
&__avatar
width: 48px
height: 48px
&__name
font-weight: bold
```
**Correct (Block name matches component selector):**
```typescript
// ✅ Component selector is 'app-user-profile', BEM block is 'user-profile'
// Strip the prefix ('app-') to get the BEM block name
@Component({
selector: 'app-user-profile',
template: `
...
{{ name }}
`
})
export class UserProfileComponent {}
```
```css
/* ✅ CSS - Block name matches component selector (without prefix) */
.user-profile {
display: flex;
padding: 16px;
}
.user-profile__avatar {
width: 48px;
height: 48px;
}
.user-profile__name {
font-weight: bold;
}
```
```scss
// ✅ SCSS - Block name matches component selector (without prefix)
.user-profile {
display: flex;
padding: 16px;
&__avatar {
width: 48px;
height: 48px;
}
&__name {
font-weight: bold;
}
}
```
```sass
// ✅ SASS - Block name matches component selector (without prefix)
.user-profile
display: flex
padding: 16px
&__avatar
width: 48px
height: 48px
&__name
font-weight: bold
```
**Using :host as the Block:**
```typescript
// ✅ Even better: use :host as the Block, elements inside use the block name
@Component({
selector: 'app-user-profile',
template: `
...
{{ name }}
`,
styles: [`
:host {
display: flex;
padding: 16px;
}
.user-profile__avatar {
width: 48px;
height: 48px;
}
.user-profile__name {
font-weight: bold;
}
`]
})
export class UserProfileComponent {}
```
**Naming convention:**
| Component Selector | BEM Block Name |
|-------------------|----------------|
| `app-user-profile` | `user-profile` |
| `app-nav-bar` | `nav-bar` |
| `app-search-results` | `search-results` |
| `lib-date-picker` | `date-picker` |
**Why it matters:**
- 1:1 mapping between component and BEM block makes code navigation trivial
- Eliminates naming collisions across the application
- Angular's ViewEncapsulation already scopes styles per component, BEM block = component reinforces this
- Developers can find styles instantly by looking at the component selector
- Shared vocabulary between template, styles, and component class
Reference: [BEM Naming](https://getbem.com/naming/)
---
## Rule 2: Maximum 2 Levels of BEM Scope in a Component
**Impact: CRITICAL** — Prevents unreadable selectors and signals when to decompose components
A component's BEM structure must never exceed 2 levels: Block and Element (`.block__element`). If you find yourself needing a grandchild element (`.block__parent__child`), it is a clear signal to extract a child component. BEM elements are always direct children of the Block — never nested under other elements.
**Incorrect (Over 2 levels of BEM depth):**
```typescript
// ❌ Too many levels — "card__header__title__icon" is 4 levels deep
@Component({
selector: 'app-product-card',
template: `
★
{{ product.name }}
{{ product.desc }}
`
})
export class ProductCardComponent {}
```
```css
/* ❌ CSS - Deeply nested BEM selectors are unreadable */
.product-card__header__title__icon {
color: gold;
}
.product-card__header__title__text {
font-size: 18px;
}
.product-card__header__actions__btn {
background: blue;
}
.product-card__body__description__text {
color: #666;
}
```
```scss
// ❌ SCSS - Nesting & creates deeply chained selectors
.product-card {
&__header {
&__title {
&__icon {
color: gold;
}
&__text {
font-size: 18px;
}
}
&__actions {
&__btn {
background: blue;
}
}
}
&__body {
&__description {
&__text {
color: #666;
}
}
}
}
```
```sass
// ❌ SASS - Same problem in indented syntax
.product-card
&__header
&__title
&__icon
color: gold
&__text
font-size: 18px
&__actions
&__btn
background: blue
&__body
&__description
&__text
color: #666
```
**Correct (Flat BEM — max 2 levels: Block + Element):**
```typescript
// ✅ All elements are direct children of the block — flat structure
@Component({
selector: 'app-product-card',
template: `
★
{{ product.name }}
{{ product.desc }}
`
})
export class ProductCardComponent {}
```
```css
/* ✅ CSS - Flat BEM selectors, easy to read and maintain */
.product-card {
border: 1px solid #eee;
border-radius: 8px;
}
.product-card__header {
display: flex;
align-items: center;
padding: 16px;
}
.product-card__icon {
color: gold;
margin-right: 8px;
}
.product-card__title {
font-size: 18px;
flex: 1;
}
.product-card__action {
background: blue;
color: white;
border: none;
padding: 8px 16px;
}
.product-card__body {
padding: 0 16px 16px;
}
.product-card__description {
color: #666;
line-height: 1.5;
}
```
```scss
// ✅ SCSS - Single level of & nesting, all elements flat under block
.product-card {
border: 1px solid #eee;
border-radius: 8px;
&__header {
display: flex;
align-items: center;
padding: 16px;
}
&__icon {
color: gold;
margin-right: 8px;
}
&__title {
font-size: 18px;
flex: 1;
}
&__action {
background: blue;
color: white;
border: none;
padding: 8px 16px;
}
&__body {
padding: 0 16px 16px;
}
&__description {
color: #666;
line-height: 1.5;
}
}
```
```sass
// ✅ SASS - Single level of & nesting, all elements flat under block
.product-card
border: 1px solid #eee
border-radius: 8px
&__header
display: flex
align-items: center
padding: 16px
&__icon
color: gold
margin-right: 8px
&__title
font-size: 18px
flex: 1
&__action
background: blue
color: white
border: none
padding: 8px 16px
&__body
padding: 0 16px 16px
&__description
color: #666
line-height: 1.5
```
**The rule visualized:**
```
✅ Allowed (2 levels max):
.block
.block__element
.block__element--modifier
.block--modifier
❌ Forbidden (3+ levels):
.block__element__subelement
.block__parent__child__grandchild
```
**Why it matters:**
- `.block__parent__child` selectors are unreadable and fragile
- Flat BEM elements decouple CSS from HTML nesting — you can restructure the DOM without renaming classes
- If you need a third level, it means your component does too much — extract a child component
- Flat selectors have consistent specificity (single class), preventing specificity wars
- Searching for `.product-card__title` is easy; searching for `.product-card__header__title__text` is not
Reference: [BEM FAQ - Should I use nested elements?](https://en.bem.info/methodology/faq/#why-does-bem-not-recommend-using-elements-within-elements-block__elem1__elem2)
---
## Rule 3: Split Child Components When BEM Depth Exceeds 2 Levels
**Impact: CRITICAL** — Enforces component decomposition, improving reusability and maintainability
When a BEM structure needs more than Block + Element depth, extract the nested section into its own Angular component with its own BEM Block. Each component owns exactly one Block. This is the companion rule to "Maximum 2 levels" — it tells you _what to do_ when you exceed the limit.
**Incorrect (Monolithic component with deep BEM nesting):**
```typescript
// ❌ One component trying to handle card + header + user-info + actions
@Component({
selector: 'app-comment-card',
template: `
{{ comment.author.name }}{{ comment.date | date }}
`
})
export class ButtonGroupComponent {}
```
```scss
// ✅ SCSS - Element modifiers
.button-group {
display: flex;
gap: 8px;
&__btn {
padding: 8px 16px;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
&--primary { background: #3498db; color: white; border-color: #3498db; }
&--secondary { background: white; color: #333; }
&--disabled { opacity: 0.5; cursor: not-allowed; }
}
}
```
**Modifier cheat sheet:**
| Type | Syntax | Example | Use For |
|------|--------|---------|---------|
| Block modifier | `.block--modifier` | `.alert--error` | Variant that changes the whole block |
| Element modifier | `.block__el--modifier` | `.btn__icon--large` | Variant for a single element |
| Boolean modifier | `.block--active` | `.nav--collapsed` | On/off states |
| Key-value modifier | `.block--size-large` | `.card--theme-dark` | Named variants |
**Why it matters:**
- Modifiers always accompany the base class, ensuring base styles are always applied
- No specificity wars — `.alert--error` (one class) vs `.alert.error` (two classes) have different specificity
- Angular's `[class.x]` binding is the idiomatic way to toggle BEM modifiers
- Predictable override behavior: modifiers always build on top of the base
- Easy to search: `alert--error` finds exactly the error variant
Reference: [BEM Modifiers](https://getbem.com/naming/)
---
## Rule 6: Avoid Cascading and Descendant Selectors with BEM
**Impact: HIGH** — Eliminates specificity conflicts and keeps styles independent of DOM structure
Never use descendant selectors (`.parent .child`), child selectors (`.parent > .child`), or tag-qualified selectors (`div.block`) with BEM. Each BEM class is unique and self-describing — it should work regardless of DOM nesting. The only exception is block modifiers affecting child elements (`.block--modifier .block__element`).
**Incorrect (Cascading and descendant selectors):**
```typescript
// ❌ Relying on DOM hierarchy for styling
@Component({
selector: 'app-sidebar',
template: `
`
})
export class SidebarComponent {}
```
```css
/* ❌ CSS - Descendant selectors create DOM-dependent, fragile styles */
.sidebar ul {
list-style: none;
}
.sidebar ul li {
padding: 8px;
}
.sidebar ul li a {
color: #333;
text-decoration: none;
}
.sidebar ul ul {
padding-left: 20px;
}
.sidebar ul ul li a {
font-size: 14px;
color: #666;
}
nav.sidebar {
width: 250px;
}
div.sidebar__item {
margin: 4px 0;
}
```
```scss
// ❌ SCSS - Nesting creates deep descendant selectors
.sidebar {
ul {
list-style: none;
li {
padding: 8px;
a {
color: #333;
text-decoration: none;
}
ul {
padding-left: 20px;
li a {
font-size: 14px;
color: #666;
}
}
}
}
}
```
```sass
// ❌ SASS - Same cascading problem
.sidebar
ul
list-style: none
li
padding: 8px
a
color: #333
text-decoration: none
ul
padding-left: 20px
li a
font-size: 14px
color: #666
```
**Correct (Flat BEM selectors, no cascading):**
```typescript
// ✅ Each element has its own unique BEM class — no DOM dependency
@Component({
selector: 'app-sidebar',
template: `
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SidebarComponent {}
```
```typescript
// ✅ Submenu extracted to own component with its own BEM block
@Component({
selector: 'app-sidebar-submenu',
template: `
{{ comment.text }}