Sass & SCSS
Angular compiles SCSS out of the box, so the real work is pairing Sass authoring features with component style encapsulation
Modern Angular only No equivalent exists in the original 100 Days series. Written fresh for Angular v22.
Lead with this: Angular has first-class SCSS support out of the box —
no extra webpack config needed. The real skill is combining SCSS's authoring
power (variables, mixins, nesting, @use/@forward) with Angular's
encapsulation system (:host, CSS custom properties) to keep styles
both reusable and properly scoped.
What it is
Sass is a CSS preprocessor — it adds variables, nesting, mixins, functions,
and a module system on top of CSS, then compiles down to plain CSS at build
time. Angular projects use the SCSS syntax (the .scss extension), which
is a superset of CSS: every valid .css file is also valid .scss.
Angular's CLI and build tooling process .scss files automatically via
esbuild's built-in Sass support — no webpack config, no extra loaders,
no extra packages to install when you choose SCSS as your style format.
How it works under the hood
Old approach — global CSS with specificity wars
In traditional multi-page websites, all CSS is global. To style a .card
inside a specific section, you write:
.admin-section .card { border: 2px solid red; }Specificity fights between component teams are common. A button style from
one team overwrites another team's button with a more specific selector.
The solution — !important — makes things worse. Global CSS is effectively
shared mutable state that every stylesheet in the app writes to.
SCSS helped (variables, nesting, partials) but didn't solve the global scoping problem — compiled SCSS is still global CSS.
Angular's emulated encapsulation — the real isolation
Angular solves global CSS pollution at the component boundary, not at the preprocessor level. When you write SCSS for a component, Angular adds a unique attribute selector to every rule at build time, scoping it to that component's DOM:
// component.scss (what you write)h1 { color: red; }.card { padding: 16px; }/* Compiled output Angular ships to the browser */h1[_ngcontent-abc-c42] { color: red; }.card[_ngcontent-abc-c42] { padding: 16px; }The _ngcontent-abc-c42 attribute is added to every element in the
component's template. The rule only matches when an element has BOTH the
class/tag AND this unique attribute — so it only affects this component's
DOM, never leaking to siblings or children.
This is emulated encapsulation — Angular emulates Shadow DOM scoping with attribute selectors, making it work on all browsers without requiring native Shadow DOM.
Sass compiles first, Angular scopes second
The build pipeline is sequential:
component.scss ↓esbuild's Sass compiler processes @use, @forward, variables, mixins, nesting ↓plain CSS (with specificity from nesting, variables replaced with values) ↓Angular's compiler adds [_ngcontent-xxx] to every rule ↓Scoped CSS sent to the browserThis means all SCSS features work normally — Angular sees the compiled CSS, not the SCSS source.
Setup
New project with SCSS
ng new my-app --style=scssSet SCSS as default for an existing project
# Set default style for future ng generate commandsng config schematics.@schematics/angular:component.style scss
# Or edit angular.json directly:# "schematics": {# "@schematics/angular:component": {# "style": "scss"# }# }// angular.json — global styles"styles": [ "src/styles.scss" // your global SCSS entry point]Component style setup
Angular 17+ uses styleUrl (singular string) instead of the old styleUrls
(array). Both work, but the singular form is the current convention:
// Modern (v17+ convention — styleUrl singular)@Component({ selector: 'app-card', standalone: true, templateUrl: './card.component.html', styleUrl: './card.component.scss', // singular string})export class CardComponent {}
// Also valid — inline styles@Component({ selector: 'app-badge', standalone: true, template: `<span class="badge">{{ label }}</span>`, styles: [` .badge { padding: 4px 8px; border-radius: 4px; background: var(--badge-bg, #e0e0e0); } `],})export class BadgeComponent {}Basic SCSS in Angular components
Variables and nesting
// card.component.scss$card-radius: 8px;$card-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
.card { border-radius: $card-radius; box-shadow: $card-shadow; overflow: hidden;
// Nesting — compiles to .card .card__header .card__header { padding: 16px; border-bottom: 1px solid #e0e0e0;
h2 { margin: 0; font-size: 1.25rem; } }
.card__body { padding: 16px; }
// Modifier — compiles to .card.card--elevated &--elevated { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); }}Mixins
// _mixins.scss (partial — leading underscore means "not compiled standalone")@mixin flex-center { display: flex; align-items: center; justify-content: center;}
@mixin responsive-grid($min-width: 280px) { display: grid; grid-template-columns: repeat(auto-fill, minmax($min-width, 1fr)); gap: 16px;}
@mixin button-variant($bg, $color: white) { background: $bg; color: $color;
&:hover { background: darken($bg, 10%); // Sass color function }
&:disabled { opacity: 0.5; cursor: not-allowed; }}// button.component.scss@use './mixins' as m;
.btn-primary { @include m.button-variant(#1976d2); padding: 8px 24px; border-radius: 4px;}The @use / @forward module system
Sass deprecated @import — it pollutes the global namespace and causes all
imported members to be visible everywhere. The replacement is @use and
@forward, which create a proper module system.
@use — import a module for use in the current file only:
// component.scss@use './variables' as vars; // access as vars.$color-primary@use './mixins' as *; // access without prefix (careful with collisions)@use 'sass:math'; // built-in Sass math module@use 'sass:color'; // built-in Sass color module
.container { color: vars.$color-primary; padding: math.div(32px, 2); // 16px — use math.div() not /}@forward — re-export members from other files (for building index files):
// styles/_index.scss — exposes everything to consumers@forward './variables';@forward './mixins';@forward './breakpoints';// component.scss — import everything through the index@use '../styles' as s;
.card { color: s.$primary-color; @include s.flex-center;}Why not @import?
// ❌ @import — deprecated, global namespace pollution@import './variables'; // $primary-color now globally available, may clash
// ✅ @use — explicit, scoped, controlled@use './variables' as vars; // only accessible as vars.$primary-color in this fileAngular-specific selectors
:host — style the component element itself
The :host selector targets the component's own host element (the
<app-card> tag in the parent's DOM). Use it for display properties,
positioning, or base styles that apply to the component's outermost wrapper:
// card.component.scss:host { display: block; // Custom elements are inline by default container-type: inline-size; // CSS container queries on the host}
:host(.expanded) { // :host with a class condition max-height: none;}
:host([disabled]) { // :host with an attribute condition opacity: 0.5; pointer-events: none;}:host-context — style based on ancestor theme
:host-context() applies styles when an ancestor matches the selector.
Useful for dark mode or theme-aware components. Note: :host-context is
deprecated in native browser CSS, but Angular's emulated encapsulation
implements it via attribute selectors so it works reliably:
// card.component.scss:host-context(.dark-theme) { background: #1e1e1e; color: #ffffff; border-color: #333;}
:host-context([data-theme="compact"]) { .card__body { padding: 8px; // tighter padding in compact theme }}::ng-deep — breaking encapsulation (deprecated, use sparingly)
::ng-deep stops Angular's encapsulation scoping at that point in the
selector, making the rule global beyond that point. Deprecated, but won't
be removed until CSS @scope reaches full browser support (Angular v22:
still functional):
// ❌ Dangerous — fully global, leaks everywhere::ng-deep .mat-button { color: red; }
// ❌ Still leaks outside the component — Angular only checks the :host part:host ::ng-deep .mat-button { color: red; }// Actually: :host limits the match to elements inside this component's// host subtree — that IS the right pattern. Use this form, not bare ::ng-deep.
// ✅ The correct ::ng-deep pattern — scoped to this component's subtree:host { ::ng-deep .mat-mdc-form-field { width: 100%; }}The decision flow for styling child/third-party components:
Does the component expose CSS custom properties? → Use them (best)Does the component expose ::part()? → Use ::part() (second)Is it your own component? → Add a CSS custom property hookIs it a third-party component? → Consider ViewEncapsulation.NoneLast resort: :host { ::ng-deep { ... } }CSS custom properties — the modern theming approach
CSS custom properties (variables) pierce Shadow DOM and emulated encapsulation
naturally — they're inherited by all descendants without ::ng-deep:
// Parent component defines the theme:host { --card-bg: #ffffff; --card-border-color: #e0e0e0; --card-padding: 16px;}
:host(.elevated) { --card-bg: #f8f8f8;}// card.component.scss — reads values provided by parent or design system.card { background: var(--card-bg, #ffffff); // fallback if not set border: 1px solid var(--card-border-color, #e0e0e0); padding: var(--card-padding, 16px);}This is how Angular Material v3 themes work — Material components expose CSS
custom properties like --mat-sys-primary and --mdc-filled-button-container-color
that you override from your component's :host block.
Global styles vs component styles
// src/styles.scss — global styles (applies everywhere)// ✅ Good uses: resets, typography scale, CSS custom property definitions, utility classes
:root { --color-primary: #1976d2; --color-primary-dark: #115293; --spacing-unit: 8px;}
body { font-family: 'Inter', sans-serif; font-size: 16px; line-height: 1.5; margin: 0;}
// Utility classes used throughout the app.visually-hidden { position: absolute; width: 1px; height: 1px; clip: rect(0, 0, 0, 0); overflow: hidden;}// component.component.scss — component-scoped styles// ✅ Anything that only applies to this component's DOM
:host { display: block; }
.inner-layout { /* ... */ }Keep global styles to: CSS custom property definitions, typography, resets, utility classes, and third-party overrides. Everything else belongs in component files.
Real-world patterns
Pattern 1 — Design token file structure
src/└── styles/ ├── _tokens.scss ← CSS custom property definitions + Sass variables ├── _typography.scss ← font scales, heading styles ├── _mixins.scss ← reusable mixins ├── _breakpoints.scss ← responsive breakpoint helpers ├── _index.scss ← @forward all of the above └── styles.scss ← global entry point (uses _index.scss)// styles/_tokens.scss$primary: #1976d2;$on-primary: #ffffff;
:root { --color-primary: #{$primary}; --color-on-primary: #{$on-primary}; --spacing-1: 4px; --spacing-2: 8px; --spacing-4: 16px; --spacing-8: 32px; --radius-sm: 4px; --radius-md: 8px; --radius-lg: 16px;}Pattern 2 — Responsive component with container queries
// product-card.component.scss:host { display: block; container-type: inline-size; // enables container queries on the host}
.card { display: grid; grid-template-columns: 1fr; gap: var(--spacing-2);}
@container (min-width: 320px) { .card { grid-template-columns: 120px 1fr; // image + content side by side
.image { grid-row: 1 / 3; } }}Container queries let a component respond to its OWN size — not the viewport. This is the correct tool for reusable cards and widgets that appear in different layout contexts.
Common mistakes
Mistake 1 — Forgetting :host for display properties
Custom HTML elements (<app-card>) are display: inline by default. Without
setting display: block (or flex, grid) on :host, the component won't
occupy space as you expect:
// ❌ Not set — app-card renders as inline, margin/width don't work as expected// component.component.scss empty or missing :host { display }
// ✅ Always set a display value on :host for block-level components:host { display: block; // or flex, grid, contents — whatever fits}Mistake 2 — Using bare ::ng-deep without :host prefix
Bare ::ng-deep without :host creates fully global styles — they leak out
of the component and affect matching elements anywhere in the app:
// ❌ Global — affects ALL .mat-button elements in the ENTIRE app::ng-deep .mat-button { font-weight: bold; }
// ✅ Scoped to this component's subtree:host { ::ng-deep .mat-button { font-weight: bold; }}Mistake 3 — Using @import instead of @use
Sass deprecated @import. In Sass 3.0 (future), it will be removed. Switch
to @use:
// ❌ Deprecated@import './variables';@import './mixins';
// ✅ Modern module system@use './variables' as vars;@use './mixins' as m;Mistake 4 — Deeply nested SCSS that defeats encapsulation intent
Deeply nesting selectors in SCSS creates high-specificity CSS that's hard to override and breaks encapsulation semantics:
// ❌ 4-level nesting — compiled to .card .header .title span { ... }// High specificity, hard to override, not Angular's intended component pattern.card { .header { .title { span { font-size: 0.875rem; } } }}
// ✅ Flat BEM-style or direct element selectors.card__title-note { font-size: 0.875rem;}
// ✅ Or use CSS custom properties and let child components define their own stylesHow this evolved
-
Angular 2–5 (2016–2017): SCSS support via
angular-cli.jsonand webpack-sass-loader. Encapsulation worked but required explicit plugin setup and was configured separately from the rest of the build. -
Angular 6 (2018):
angular.jsonreplaced.angular-cli.json. SCSS style support consolidated.styleUrlsas an array was the norm. -
Angular 17 (2023):
styleUrl(singular) introduced alongside the esbuild migration. Built-in Sass processing via esbuild — no webpack sass-loader, significantly faster SCSS compilation. -
Angular 17+ (2023–present): Container queries encouraged as the modern responsive tool for components. CSS custom properties became the standard theming API (Angular Material v3 adopted them fully).
::ng-deepguidance strengthened — use CSS custom properties instead. -
Angular 22 (now):
styleUrlis the standard. esbuild handles SCSS natively. Sass@use/@forwardmodule system is the standard (@importdeprecated by Sass).::ng-deepremains deprecated but functional. The recommended theming architecture is: CSS custom properties at the:rootor:hostlevel, consumed viavar()in component styles.
See also
- View Encapsulation — deep dive into Emulated vs ShadowDom vs None encapsulation modes
- Angular Material — theming Material components with CSS custom properties
- Official docs — Component styles
- Sass @use and @forward docs
- CSS custom properties — MDN