CSS препроцесори: SASS, SCSS та LESS
CSS препроцесори компілюють розширений синтаксис CSS зі змінними, вкладеністю, міксинами та функціями у звичайний CSS, який розуміє браузер.
Теорія
TL;DR
- Препроцесор схожий на текстовий редактор з шаблонами: пишеш структуровані файли з багаторазовими блоками, компілюєш у plain CSS для браузера.
- SCSS є строгою надмножиною CSS (будь-який валідний CSS є валідним SCSS); SASS прибирає фігурні дужки на користь відступів; LESS використовує
@для змінних і компілюється через JS. - Головний поділ: SCSS і LESS використовують фігурні дужки та крапки з комою, SASS - тільки відступи.
- Правило вибору: SCSS для команд (міграція CSS без змін); LESS для динамічного JS-теміфікування; обидва можна пропустити, якщо вистачає CSS custom properties.
- Bootstrap 5.3, Angular 18 та Bulma поставляються з SCSS.
Швидкий приклад
// input.scss
$blue: #007bff;
.nav {
background: $blue;
.item {
padding: 1rem;
&:hover {
background: lighten($blue, 10%); // обчислюється до #4dabf7
}
}
}/* Скомпільований результат */
.nav { background: #007bff; }
.nav .item { padding: 1rem; }
.nav .item:hover { background: #4dabf7; }Компілятор читає вкладену структуру, вирівнює її у стандартні селектори і підставляє всі змінні за один прохід.
Головна різниця
SCSS на 100% сумісний з CSS. Вставляєш будь-який існуючий stylesheet у .scss файл - він компілюється без жодних правок. Саме це робить міграцію простою. Оригінальний SASS замінює фігурні дужки та крапки з комою на відступи, що деяким розробникам подобається за мінімалістичний вигляд. LESS зовні схожий на SCSS, але використовує синтаксис @variable, і його JS-компілятор може давати несподівані результати у складних циклах на великих кодових базах. Більшість React-проектів, з якими я працював, зупинялись на SCSS щойно кількість компонентів переходила за 20 - просто тому, що наявний CSS вставляється без правок.
Коли що використовувати
- Командний проект з наявним CSS: SCSS - мігруєш файли без змін.
- Динамічне теміфікування через JS (як у Ant Design 5): LESS - його
@varдобре інтегрується з JS-конфігурацією. - Малий проект або прототип: пропускай препроцесори, використовуй CSS custom properties (
--color: red;) - підтримуються всіма сучасними браузерами з 2016 року. - Angular або React/Vite: будь-який варіант -
sass-loaderабо вбудована підтримка Vite впораються з компіляцією. - Ruby-стек: SASS - найглибша екосистема там.
Таблиця порівняння
| Особливість | SASS (.sass) | SCSS (.scss) | LESS (.less) |
|---|---|---|---|
| Синтаксис | Тільки відступи | Фігурні дужки + крапки з комою | Фігурні дужки + крапки з комою |
| Надмножина CSS | Ні | Так (100%) | Так (здебільшого) |
| Змінні | $var | $var | @var |
| Міксини | @mixin | @mixin | .mixin() |
| Компіляція | Ruby/Node | Ruby/Node/Dart | JS/Node/браузер |
| Цикли | @each, @for | @each, @for | Guards, рекурсивні міксини |
| Коли використовувати | Ruby-розробники | Команди, міграція CSS | JS-проекти, динамічні змінні |
Як працює компіляція
Ти пишеш .scss, .sass або .less файли. Компілятор (Dart Sass для SCSS/SASS починаючи з v1.77+, lessc для LESS v4.1+) розбирає вихідний код у AST (abstract syntax tree), підставляє всі змінні та виклики міксинів за один прохід і видає стандартний CSS. sass-loader для Webpack v14+ або вбудована підтримка Vite слідкують за файлами і перекомпіловують при збереженні, записуючи результат у /dist. Браузер ніколи не бачить вихідних файлів препроцесора.
Типові помилки
Вкладеність глибше 3-4 рівнів
// Неправильно - компілюється у .a .b .c .d .e { color: red; }
a { b { c { d { e { color: red; } } } } }Глибока вкладеність роздуває скомпільований вивід і уповільнює пошук селекторів. Chrome DevTools позначає селектори глибше 6 рівнів. Тримай вкладеність до 3 рівнів або використовуй BEM: .nav__item--active { color: red; }.
Глобальне перевизначення змінних між файлами
// _theme.scss
$color: blue;
// _component.scss імпортує _theme.scss, потім:
$color: red; // перевизначає глобальноЧасткова перекомпіляція у Webpack може пропустити залежності і дати непослідовний результат. Використовуй @use 'theme' as *; з Sass modules (v1.23+). Це виокремлює простір імен змінних і обчислює кожен файл лише один раз.
@import замість @use
@import скидає все у глобальний простір імен і повторно виконує файл при кожному імпорті. @use завантажує файл один раз і дає доступ через простір імен (theme.$primary). Dart Sass позначив @import як застарілий починаючи з v1.80.
Компіляція LESS у браузері
Стара практика підключення less.js безпосередньо у браузері не генерує source maps і ламається на сучасних міксинах. Завжди компілюй наперед за допомогою lessc або бандлера.
Ліниве обчислення у циклах LESS
// LESS - guard-based цикл може пропускати значення
.loop(@n) when (@n > 0) {
.w-@{n} { width: ~"@{n}%"; }
.loop((@n - 5));
}
.loop(25);Для надійних циклів @for у SCSS поводиться передбачуваніше:
@for $i from 1 through 5 {
.w-#{$i * 5} { width: $i * 5%; }
}Де зустрічається у реальних проектах
- Bootstrap 5.3: SCSS змінні та міксини для теміфікування (
$primary,@mixin button-variant). - Ant Design 5: LESS для динамічного теміфікування з JS-конфігурацією змінних.
- Angular 18: вбудована підтримка SCSS з вкладеністю для
:host. - Bulma 1.0: чистий SCSS з CSS custom properties як fallback.
- React/Vite:
sass-loaderплюс SCSS modules на рівні компонентів.
Питання на співбесіді
Q: Яка різниця між @mixin і @extend у SCSS?
A: @mixin копіює оголошені стилі у кожен селектор, який його підключає - це може роздути скомпільований вивід. @extend змушує кілька селекторів спільно використовувати один блок правил, що компактніше, але чутливо до порядку. Використовуй @extend для утилітарних класів на кшталт .btn; @mixin - коли потрібно передавати аргументи.
Q: Як обробляти vendor prefixes при роботі з препроцесорами?
A: Не безпосередньо через препроцесор. Стандартна практика - запускати Autoprefixer як PostCSS плагін після компіляції. Він читає налаштування browserslist і додає -webkit-, -moz- тощо автоматично.
Q: Яка різниця між @use і @import у Sass?
A: @import скидає все у глобальний простір імен і повторно виконує файл при кожному імпорті. @use завантажує файл один раз і виокремлює змінні через простір імен (theme.$primary). З'явився у Dart Sass v1.23; @import визнано застарілим з v1.80.
Q: Чим Dart Sass відрізняється від LibSass?
A: Dart Sass - офіційна реалізація (v1.77+ станом на 2024), точно слідує специфікації. LibSass - C++ порт, визнаний застарілим у 2021 через відставання від @use і @forward. Якщо ще використовуєш node-sass - мігруй на пакет sass.
Q (senior): У монорепо зі 100+ SCSS партіалів як уникнути каскадних перезбірок?
A: Використовуй @use замість @import, щоб кожен партіал обчислювався один раз. Поєднуй з Vite ?inline імпортами для HMR на рівні компонентів. Повільні збірки профілюй через sass --trace. Уникай glob-імпортів - вони ламають відстеження залежностей.
Приклади
Навбар з брейкпоінтами та темою
React-застосунок: навбар адаптується до розміру екрана і бере кольори зі спільного файлу теми.
// styles/navbar.scss
$primary: #0d6efd;
$breakpoint-md: 768px;
@mixin flex-center {
display: flex;
align-items: center;
}
.navbar {
@include flex-center;
padding: 1rem;
background: $primary;
@media (min-width: $breakpoint-md) {
padding: 1.5rem;
}
.logo { font-size: 1.5rem; }
&__link {
color: white;
text-decoration: none;
&:hover {
color: lighten($primary, 20%);
}
}
}/* Скомпільований результат (спрощено) */
.navbar { display: flex; align-items: center; padding: 1rem; background: #0d6efd; }
@media (min-width: 768px) { .navbar { padding: 1.5rem; } }
.navbar .logo { font-size: 1.5rem; }
.navbar__link { color: white; text-decoration: none; }
.navbar__link:hover { color: #6ea8fe; }Міксин бере на себе flex-налаштування, $primary оголошено один раз, а медіа-запит знаходиться поруч з компонентом, якого він стосується, а не в окремому файлі брейкпоінтів.
Sass modules з @use
Цей підхід замінює @import у будь-якому Dart Sass проекті (v1.23+) і є поточною рекомендованою практикою.
// _theme.scss
$primary: #0d6efd;
$danger: #dc3545;
$font-size-base: 1rem;// button.scss
@use 'theme';
.btn {
font-size: theme.$font-size-base;
background: theme.$primary;
&--danger {
background: theme.$danger;
}
}Префікс theme. одразу показує, звідки береться кожна змінна. Не доведеться шукати по 50 партіалах, який файл востаннє встановив $primary.
Цикли: SCSS проти LESS
Генерація utility-класів для відступів - типове завдання. Ось як кожен препроцесор з ним справляється.
// SCSS - передбачуваний @for цикл
@for $i from 1 through 5 {
.mt-#{$i} {
margin-top: $i * 0.25rem; // 0.25, 0.5, 0.75, 1, 1.25rem
}
}// LESS - рекурсивний міксин з guard (аналогічний результат)
.spacing-loop(@n) when (@n > 0) {
.mt-@{n} { margin-top: (@n * 0.25rem); }
.spacing-loop((@n - 1));
}
.spacing-loop(5);Обидва варіанти генерують однакові п'ять класів. SCSS версія читається як звичайний цикл; LESS версія вимагає розуміння рекурсії через guards, перш ніж безпечно її змінювати.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.