Chapter 22: SASS / SCSS Tutorial & Modern CSS Architecture
SASS (Syntactically Awesome Style Sheets) supercharges stylesheet development with modern module systems (@use, @forward), SASS maps, control directives (@each, @for, @if), inheritance (@extend), custom functions (@function), parameterized mixins (@mixin + @content), and BEM (Block Element Modifier) architectural patterns for scalable design token systems.
22.1 SASS / SCSS Feature Reference Guide
| SCSS Concept | Syntax Pattern | Purpose & Best Practice |
|---|---|---|
| Module System | @use "sass:color"; @use "tokens" as t; |
Replaces legacy @import with scoped namespace imports. |
| Forwarding | @forward "config/variables"; |
Aggregates and re-exports multiple sub-modules into a unified entry point. |
| Sass Maps | $theme-colors: (primary: #6366f1, success: #10b981); |
Key-value data structures storing design system tokens. |
| Loops (@each) | @each $name, $color in $theme-colors { ... } |
Dynamically iterates over Sass maps to generate utility classes. |
| Loops (@for) | @for $i from 1 through 12 { .col-#{$i} { ... } } |
Generates grid column layout utility classes. |
| Mixins & Include | @mixin flex-center($gap: 10px) { ... } @include flex-center(16px); |
Encapsulates reusable CSS code blocks with parameter defaults. |
| Extend / Placeholder | %btn-base { ... } .btn-primary { @extend %btn-base; } |
Shares CSS rules between selectors without duplication. |
| Custom Functions | @function px-to-rem($px) { @return math.div($px, 16) * 1rem; } |
Returns computed values; unlike mixins, functions return a single value. |
| Control Flow | @if $theme == dark { color: #fff; } @else { color: #000; } |
Conditional logic for theme switching and responsive token generation. |
| Debugging | @debug "Value: #{$size}"; / @warn "Deprecated!"; |
Prints runtime values to the Sass compiler console for debugging. |
22.2 SASS Module System (@use & @forward)
Modern SASS deprecated global @import due to namespace pollution and double-compilation performance bugs. The modern @use and @forward module rules provide scoped namespaces:
/* _colors.scss */$primary: #6366f1;/* _index.scss (barrel file — re-exports all partials) */@forward "colors";@forward "typography";/* main.scss */@use "tokens" as t; /* loads _index.scss implicitly */.btn { background: t.$primary; } /* Access via scoped namespace 't' */
@import is Deprecated
@import was officially removed in Dart Sass 2.0. Always use @use for importing modules and @forward for re-exporting. Files prefixed with _ (partials) are never compiled to standalone CSS files.
22.3 Sass Maps & Dynamic Utility Generation Loops (@each, @for)
Storing design tokens inside Sass maps allows generating entire utility class libraries (e.g. .bg-primary, .text-success, .col-6) automatically using @each and @for loops:
<!DOCTYPE html>
<html>
<head>
<style>
*, *::before, *::after { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; background: #0f172a; color: #f8fafc; padding: 24px; margin: 0; }
h4 { color: #f59e0b; margin-top: 0; }
/* Compiled SCSS Output from @each $name, $color loop over $theme-colors map:
$theme-colors: (
"indigo": #6366f1,
"cyan": #06b6d4,
"emerald": #10b981,
"rose": #f43f5e
);
@each $name, $color in $theme-colors {
.badge-#{$name} { background: rgba($color, 0.2); color: lighten($color, 20%); border: 1px solid $color; }
}
*/
.badge-indigo { background: rgba(99, 102, 241, 0.2); color: #a5b4fc; border: 1px solid #6366f1; }
.badge-cyan { background: rgba(6, 182, 212, 0.2); color: #38bdf8; border: 1px solid #06b6d4; }
.badge-emerald { background: rgba(16, 185, 129, 0.2); color: #34d399; border: 1px solid #10b981; }
.badge-rose { background: rgba(244, 63, 94, 0.2); color: #fb7185; border: 1px solid #f43f5e; }
.badge { padding: 8px 14px; border-radius: 6px; font-weight: bold; font-size: 12px; display: inline-block; margin-right: 8px; }
/* Compiled SCSS Output from @for $i from 1 through 4 loop:
@for $i from 1 through 4 {
.col-#{$i} { width: calc(100% / 4 * #{$i}); }
}
*/
.col-1 { width: 25%; } .col-2 { width: 50%; } .col-3 { width: 75%; } .col-4 { width: 100%; }
.col-demo { background: #1e293b; border: 1px solid #334155; border-radius: 6px; padding: 8px; font-size: 11px; color: #64748b; text-align: center; margin-top: 8px; }
</style>
</head>
<body>
<h4>Sass Map @each Utility Generation Output</h4>
<p style="font-size:12px;color:#64748b;">Badges generated dynamically from Sass token map iteration:</p>
<span class="badge badge-indigo">Indigo Token</span>
<span class="badge badge-cyan">Cyan Token</span>
<span class="badge badge-emerald">Emerald Token</span>
<span class="badge badge-rose">Rose Token</span>
<h4 style="margin-top:24px;">@for Loop Grid Column Output</h4>
<div class="col-1 col-demo">.col-1 (25%)</div>
<div class="col-2 col-demo">.col-2 (50%)</div>
<div class="col-3 col-demo">.col-3 (75%)</div>
<div class="col-4 col-demo">.col-4 (100%)</div>
</body>
</html>
22.4 BEM Architecture (Block Element Modifier) & SCSS Nesting
BEM stands for Block, Element, Modifier. In SCSS, the parent reference selector & allows nesting elements (&__element) and modifiers (&--modifier) seamlessly:
<!DOCTYPE html>
<html>
<head>
<style>
*, *::before, *::after { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; background: #0f172a; color: #f8fafc; padding: 24px; margin: 0; }
h4 { color: #10b981; }
/* SCSS Source:
.card {
background: #1e293b; border-radius: 12px; padding: 20px;
&--featured { border: 2px solid #f59e0b; background: rgba(245,158,11,0.15); }
&__title { color: #fbbf24; margin: 0 0 6px; }
&__body { color: #94a3b8; font-size: 13px; margin: 0; }
}
*/
.card { background: #1e293b; border-radius: 12px; padding: 20px; max-width: 340px; margin: 0 auto 16px; text-align: center; }
.card--featured { border: 2px solid #f59e0b; background: rgba(245,158,11,0.15); }
.card__title { color: #fbbf24; margin: 0 0 6px; }
.card__body { color: #94a3b8; font-size: 13px; margin: 0; }
</style>
</head>
<body>
<h4 style="text-align:center;">BEM & SCSS Parent Selector (&) Demo</h4>
<div class="card">
<h4 class="card__title">.card (Base Block)</h4>
<p class="card__body">Default card using BEM block + element classes.</p>
</div>
<div class="card card--featured">
<h4 class="card__title">.card--featured (Modifier)</h4>
<p class="card__body">Styled using BEM SCSS nested modifier pattern!</p>
</div>
</body>
</html>
22.5 @extend & Placeholder Selectors (%)
@extend allows one selector to inherit all CSS rules from another selector. Placeholder selectors (prefixed with %) are "ghost" selectors that only exist in SCSS — they never compile to CSS on their own, making them the ideal base for shared styles:
| Pattern | SCSS Source | Compiled CSS Output | When to Use |
|---|---|---|---|
| @extend a class | .btn-primary { @extend .btn; color: #fff; } |
.btn, .btn-primary { /* shared rules */ } .btn-primary { color: #fff; } |
When base class already exists in CSS output |
| Placeholder % | %btn-base { padding: 10px 20px; } .btn-primary { @extend %btn-base; } |
.btn-primary { padding: 10px 20px; } — no %btn-base in output |
When base is a shared abstract — never rendered directly |
| @extend vs @mixin | @extend shares selector groups; @mixin copies declarations inline | @extend produces grouped selectors; @mixin duplicates declarations | Use @extend for identical static rules; @mixin for parameterized output |
/* SCSS Source */
%alert-base {
padding: 14px 18px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.alert-success {
@extend %alert-base;
background: rgba(16, 185, 129, 0.15);
border: 1px solid #10b981;
color: #34d399;
}
.alert-danger {
@extend %alert-base;
background: rgba(239, 68, 68, 0.15);
border: 1px solid #ef4444;
color: #f87171;
}
/* -------- Compiled CSS Output -------- */
/* %alert-base does NOT appear — only the concrete selectors: */
.alert-success, .alert-danger {
padding: 14px 18px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.alert-success { background: rgba(16,185,129,0.15); border: 1px solid #10b981; color: #34d399; }
.alert-danger { background: rgba(239,68,68,0.15); border: 1px solid #ef4444; color: #f87171; }
Key Rule: Never @extend Across Media Queries
@extend cannot be used inside @media blocks to extend selectors defined outside that block. This causes a Sass compiler error. Use @mixin + @include inside media queries instead.
22.6 Advanced Mixins — @content Blocks & Responsive Breakpoint Patterns
The @content directive inside a @mixin acts as a slot — it allows the caller to pass an entire block of CSS rules into the mixin at the point of @include. This is the cornerstone pattern for building responsive breakpoint mixins:
/* SCSS Source — Responsive Breakpoint Mixin Library */
$breakpoints: (
"sm": 576px,
"md": 768px,
"lg": 1024px,
"xl": 1280px
);
/* @content mixin — caller passes their CSS block */
@mixin respond-to($bp) {
$size: map.get($breakpoints, $bp);
@if $size == null {
@warn "Unknown breakpoint: #{$bp}. Valid keys: sm, md, lg, xl.";
} @else {
@media (min-width: $size) {
@content; /* <-- caller's CSS block inserted here */
}
}
}
/* Usage */
.hero-title {
font-size: 1.5rem; /* mobile-first: base */
@include respond-to("md") {
font-size: 2.5rem; /* tablet+ override */
}
@include respond-to("xl") {
font-size: 4rem; /* desktop+ override */
}
}
/* -------- Compiled CSS Output -------- */
.hero-title { font-size: 1.5rem; }
@media (min-width: 768px) { .hero-title { font-size: 2.5rem; } }
@media (min-width: 1280px) { .hero-title { font-size: 4rem; } }
@content with Arguments (Sass 1.15+)
Modern Sass allows passing arguments back to the @content block using @content($arg) and consuming them with @include mixin using ($arg) { ... }. This enables building context-aware hover/focus state mixins.
@mixin on-event($self: false) {
@if $self {
&, &:hover, &:focus, &:active { @content; }
} @else {
&:hover, &:focus, &:active { @content; }
}
}
.btn-primary {
background: #6366f1;
@include on-event() {
background: #4f46e5;
transform: translateY(-1px);
}
}
22.7 SCSS Custom Functions (@function)
SCSS @function directives return computed values (unlike @mixin which outputs CSS declarations). Functions are ideal for unit conversions, token lookups, and mathematical calculations:
/* SCSS Source — Custom Function Library */
@use "sass:math";
@use "sass:color";
/* ── Unit Conversion: px → rem ── */
$base-font-size: 16px;
@function px-to-rem($px) {
@return math.div($px, $base-font-size) * 1rem;
}
/* ── Color Utility: Tint (mix color with white) ── */
@function tint($color, $percentage) {
@return color.mix(white, $color, $percentage);
}
/* ── Color Utility: Shade (mix color with black) ── */
@function shade($color, $percentage) {
@return color.mix(black, $color, $percentage);
}
/* ── Z-Index Manager: Fetch from named layer map ── */
$z-layers: (
"base": 1,
"dropdown": 100,
"sticky": 200,
"modal": 300,
"toast": 400
);
@function z($layer) {
@if map.has-key($z-layers, $layer) {
@return map.get($z-layers, $layer);
}
@warn "Unknown z-index layer: #{$layer}";
@return null;
}
/* ── Usage Examples ── */
h1 { font-size: px-to-rem(32px); } /* → 2rem */
p { font-size: px-to-rem(14px); } /* → 0.875rem */
.badge { background: tint(#6366f1, 80%); color: shade(#6366f1, 30%); }
.modal { z-index: z("modal"); } /* → 300 */
.sticky { z-index: z("sticky"); } /* → 200 */
@function vs @mixin — When to Use Each
- Use
@functionwhen you need to return a single computed value (a number, color, string) to be used inside a property value. - Use
@mixinwhen you need to output a block of CSS declarations or multiple properties at once. - Functions cannot contain
@includestatements or output CSS rules — only@return.
22.8 Control Flow — @if / @else, @while, @debug & @warn
SCSS includes a full set of control flow directives for conditional logic and iteration, as well as compiler-level diagnostics:
| Directive | Syntax | Behaviour |
|---|---|---|
@if / @else if / @else |
@if $size == "large" { font-size: 2rem; } @else { font-size: 1rem; } |
Conditional branching. Evaluates boolean SCSS expressions. |
@while |
$i: 1; @while $i <= 5 { .mt-#{$i} { margin-top: #{$i * 4}px; } $i: $i + 1; } |
Repeats until condition is false. Use @for when loop count is known. |
@debug |
@debug "Color value: #{$primary}"; |
Prints a debug message to the Sass compiler stdout. Removed from compiled CSS. |
@warn |
@warn "Using deprecated mixin. Use respond-to() instead."; |
Prints a warning to compiler stderr. Used for deprecation notices. |
@error |
@error "Unknown theme: #{$theme}. Expected light or dark."; |
Stops compilation with a fatal error message. |
/* SCSS Source — Conditional Theming with @if/@else */
$themes: ("light", "dark", "high-contrast");
@mixin theme-tokens($theme) {
@if $theme == "dark" {
--bg: #0f172a;
--text: #f8fafc;
--border: #334155;
--accent: #6366f1;
} @else if $theme == "light" {
--bg: #ffffff;
--text: #0f172a;
--border: #e2e8f0;
--accent: #4f46e5;
} @else if $theme == "high-contrast" {
--bg: #000000;
--text: #ffffff;
--border: #ffffff;
--accent: #ffff00;
} @else {
@error "Unknown theme '#{$theme}'. Valid themes: #{$themes}";
}
@debug "Compiling theme: #{$theme}"; /* printed to terminal only */
}
/* Generate theme attribute selectors */
[data-theme="dark"] { @include theme-tokens("dark"); }
[data-theme="light"] { @include theme-tokens("light"); }
[data-theme="high-contrast"] { @include theme-tokens("high-contrast"); }
/* @while loop — generate spacing scale */
$step: 1;
@while $step <= 8 {
.mt-#{$step} { margin-top: #{$step * 4}px; }
.mb-#{$step} { margin-bottom: #{$step * 4}px; }
.p-#{$step} { padding: #{$step * 4}px; }
$step: $step + 1;
}
/* Output: .mt-1 { margin-top: 4px; } ... .p-8 { padding: 32px; } */
22.9 Real-World SCSS Folder Architecture — The 7-1 Pattern
The 7-1 Pattern (7 folders, 1 main file) is the industry-standard SCSS project architecture. All partials are aggregated into a single main.scss entry point that compiles to one styles.css bundle:
/* main.scss — Entry Point */
@use "abstracts"; /* loads abstracts/_index.scss → forwards vars, fns, mixins */
@use "base";
@use "components";
@use "layout";
@use "pages";
@use "themes";
@use "vendors";
/* Compile: npx sass styles/main.scss public/styles.css --watch */
/* Output: public/styles.css + public/styles.css.map (source map) */
SCSS Compiler Commands
npx sass styles/main.scss public/styles.css --watch— compile and watch for changesnpx sass styles/main.scss public/styles.css --style=compressed— minified production outputnpx sass --no-source-map styles/main.scss public/styles.css— skip source map generationnpx sass --load-path=node_modules styles/main.scss public/styles.css— allow importing fromnode_modules
22.10 Interactive Sandbox — SCSS Compiled Output Explorer
About This Sandbox
SCSS must be compiled server-side (via Dart Sass CLI or a build tool like Vite/Webpack). The sandbox below runs plain CSS — it shows what your SCSS compiles to. The original SCSS source is displayed in comments above each compiled rule, so you can compare source → output side by side.
The sandbox demonstrates compiled CSS output from the key SCSS patterns covered in this chapter — BEM modifier classes, @extend placeholder inheritance, and a px-to-rem() function result: