Переменные, вложенность, color-mix: что CSS забрал у Sass

стили≈ 13 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Долгое время у CSS не было ни переменных, ни вложенных правил, ни функций для цвета. Всё это приносил Sass — язык-надстройка, который перед выкладкой переводят в обычный CSS. Сегодня три эти возможности браузер умеет сам, и умеет по-своему: переменная CSS живёт в каскаде и меняется на классе, а переменная Sass исчезает ещё до того, как страница откроется. Разница не академическая — от неё зависит, можно ли поменять тему страницы по щелчку. Разберём пользовательские свойства, вложенность и color-mix(), посмотрим, что осталось за Sass, сведём в таблицу приёмы вёрстки, которые заменил сам CSS, и решим, когда странице всё-таки понадобится сборщик.

Разделы темы:·

Свойство, чьё имя начинается с двух дефисов, — пользовательское: --brand: rgb(0, 0, 255). Вы уже встречали такое в анимации переменной через @property. Сам браузер ни к чему это свойство не применяет, а подставляет его значение туда, где написано var(--brand). Имя придумываете вы, и регистр в нём важен: --brand и --Brand — два разных свойства.

Главное в нём то, что это обычное свойство CSS. Его объявляют в правиле для каких-то элементов, оно спорит в каскаде и наследуется, как цвет. Поэтому общие значения по традиции объявляют в правиле для :root. Этот псевдокласс выбирает корень документа, элемент <html>, и от корня переменная по наследству доходит до каждого элемента страницы. А переопределить её можно на любом элементе поменьше — всё внутри получит новое значение.

Ниже две кнопки с одним правилом .btn. Вторая лежит внутри блока .promo, который задаёт --brand заново. Прибор меряет и переменные: имя с двумя дефисами передают вторым аргументом, как имя свойства. Предскажите обе строки: какое значение --brand окажется у каждой кнопки, хотя на самих кнопках его никто не объявлял, и какого цвета будет их текст?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Первая кнопка получила --brand от корня, вторая — от .promo: переменная наследуется, и значение приходит от ближайшего предка, который её задал. var(--brand) подставляется уже на самой кнопке, поэтому одно правило .btn дало два цвета. На этом держатся темы: правила компонентов пишут var(--brand) и не знают, какое значение придёт сверху.

Второй аргумент var() — запасное значение: var(--w, 100px) подставит 100px, если --w не задана ни на элементе, ни у его предков. Но годится ли значение переменной для свойства, браузер проверяет только при подстановке, и это главный источник тихих поломок. --w: 150 — законная переменная: для неё это просто текст. Полосы ниже лежат в обёртке шириной 200px. У второй --w: 150px, у третьей в значении забыли единицы. Предскажите три ширины.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
У первой полосы 100px — запасное значение, у второй 150px — переменная. А третья растянулась на всю обёртку. Переменная задана, поэтому запасное значение не понадобилось. Подставленное 150 для ширины не годится, и объявление превращается в пустое место: свойство получает начальное значение из спецификации CSS, у width это auto. Наследуемое свойство, например color, взяло бы значение родителя. Не помогут ни правило width выше в коде, ни стили браузера: судья каскада уже выбрал объявление с var(), а ошибка обнаружилась позже.

Запасное значение в var() спасает от переменной, которой нет, но не от переменной с ошибкой. Опечатка в значении не выглядит ошибкой: свойство молча получает начальное значение, и правила выше в коде уже не помогут.

Пользовательские свойства — Baseline widely с 05.10.2019: пишите. Главная их сила в том, чего Sass дать не мог: переменная живёт в каскаде, поэтому её можно переопределить на классе, на одном элементе или даже из JavaScript — одной строкой на корневом элементе. Так делают темы и размеры под устройство. Про смену темы одной строкой — часть II: тема одной строкой через класс на корне и CSS-переменные.

Стили одного компонента удобно держать рядом: сама карточка, её заголовок, её вариант. Вложенность разрешает писать правило внутри правила. Вложенное .title { … } внутри .card { … } значит .card .title — заголовок где угодно внутри карточки. Знак & обозначает сам селектор внешнего правила; он нужен, когда речь не о потомке, а о той же карточке: &.title — это .card.title, один элемент с обоими классами. Им же удобно записать и другую связь: & > p — прямые дети карточки.

В примере карточка с заголовком внутри и отдельный абзац, у которого оба класса сразу. Внутри .card два вложенных правила, и отличаются они только знаком &. Предскажите оба цвета.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Заголовок внутри карточки красный: .title без знака — потомок. Абзац с двумя классами зелёный: &.title приклеился к .card без пробела. Синее правило самой .card этот абзац тоже задевает, но .card.title весит (0, 2, 0) против (0, 1, 0). Разница та же, что между .card .title и .card.title: вложенное правило без знака само ставит пробел, а & вплотную к селектору его убирает.

Вложенность пришла в CSS из Sass, и старое правило легко перенести как есть — вместе с ловушкой. В Sass популярна запись &__title внутри .card: Sass склеивает текст и получает класс card__title. Так части компонента называют по соглашению BEM — блок, два подчёркивания, элемент. Ниже запись перенесена один в один. Предскажите цвет абзаца.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Абзац чёрный. Браузер ничего не склеивает: для него & — целый селектор, а не кусок имени, поэтому правило &__title не нашло ни одного элемента, и никакой ошибки браузер не показал.

Об этой ловушке предупреждала заметка на полях в статье про селекторы и судью каскада. Там же вторая ловушка — вес знака: & весит как самый сильный селектор из списка внешнего правила, и внутри #box, .card он тянет вес id, даже если элемент нашёлся по классу.

И правило на каждый день: не вкладывайте глубже двух уровней. Каждый уровень дописывает к селектору ещё кусок. Три вложенных правила .menu, .list и .item дают селектор .menu .list .item весом (0, 3, 0). Такое правило перебьёт почти любой класс, написанный потом, и перестанет работать, как только разметку перестроят. Во второй задаче вы посчитаете, чем это кончается.

Вложенность группирует правила, а не копирует дерево разметки. Два уровня — компонент и его части; третий добавляет вес, который потом придётся перебивать.

Вложенность — Baseline widely с 11.06.2026: пишите.

Кнопке часто нужен второй цвет из того же первого: рамка потемнее, фон отключённой кнопки побледнее, оттенок при наведении. В Sass для этого брали функции lighten() и darken(): они сдвигали светлоту цвета при сборке, и результат был зашит в файл навсегда. В CSS есть color-mix() — функция, которая смешивает два цвета в заданной доле уже в браузере. color-mix(in srgb, rgb(0, 0, 255), black 20%) — синий, в который подмешали 20% чёрного. Слова in srgb называют пространство, в котором браузер смешивает: srgb — привычные доли красного, зелёного и синего. Другие пространства, например oklch, смешивают ближе к тому, как цвет видит глаз, но проверять удобнее числа srgb.

Подмешать можно и transparent — полностью прозрачный цвет. Предскажите фон трёх кнопок на странице примера. Подсказка: смешанный цвет браузер записывает не как rgb(), а color(srgb красный зелёный синий), где доли идут от 0 до 1. Прозрачность, если она есть, стоит в конце после косой черты.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Вторая кнопка темнее первой: её синий ослаб до 0.8 — это 80% синего и 20% чёрного, в котором синего нет. Третья не темнее, а прозрачнее: синий полный, а прозрачность 0.5. Прозрачный цвет не затемняет, а растворяет: сквозь такую кнопку проступает фон под ней. Доли 0.8 и 0.5 браузер считает в том пространстве, где смешивал, — в srgb.

Соберём три возможности в одном месте. Кнопка берёт фон из --brand, вариант &.danger переопределяет переменную, вложенный .icon задаёт ширину значку. Полоса .muted лежит вне кнопок и смешивает --brand с белым пополам. Предскажите три строки, а с третьей не спешите: какое значение --brand видит полоса, которая лежит снаружи?

Практический вывод из трёх возможностей короткий: то, ради чего в проект десять лет назад добавляли сборщик, сегодня по большей части умеет сам браузер. Переменные для тем, вложенность для компонентов, смешивание цветов для состояний кнопки — всё это пишется обычным CSS и не требует ни шага сборки, ни папки с зависимостями. Осталось за Sass немного, но оставшееся иногда решает всё.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Кнопка danger красная: вариант поменял переменную, а правило background-color: var(--brand) осталось одно на обе кнопки. Полоса лежит не в кнопке, и --brand пришла к ней от корня — синяя. Пополам с белым получилось color(srgb 0.5 0.5 1): половина от 255 — это 127.5, в целые числа rgb() такой цвет не ложится. В Sass так не выйдет. Функция Sass считает цвет один раз при сборке, а color-mix() с var() считается на каждом элементе заново, со своим значением переменной.

color-mix() — Baseline widely с 09.11.2025: пишите.

Sass — язык стилей поверх CSS. Файл с расширением .scss выглядит как CSS с добавками: переменные со знаком $, миксины, циклы. Браузер такой файл не читает. Перед выкладкой компилятор sass — программа, которую ставят отдельно и запускают в терминале, — переводит его в обычный .css. Ниже сеанс терминала. Строка, которая начинается со знака $ и пробела, — команда: первая печатает файл кнопки, вторая компилирует его. Что напечатал компилятор, показано под листингом.

shлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.
вывод · терминал, sass 1.104.1
.btn {
color: rgb(0, 0, 255);
border: 2px solid rgb(0, 0, 255);
}
.btn--danger {
color: rgb(255, 0, 0);
border: 2px solid rgb(255, 0, 0);
}
.btn--ok {
color: rgb(0, 128, 0);
border: 2px solid rgb(0, 128, 0);
}

Посмотрите, чего нет в результате: ни $brand, ни @mixin, ни цикла. Sass вычислил всё до браузера и оставил три готовых правила. Отсюда главное отличие от --brand: переменную Sass нельзя поменять на классе или в разметке — к тому времени, когда страница открылась, её уже нет. А имена btn--danger и btn--ok Sass собрал из &--#{$name} склейкой текста, которой, как вы видели, браузер не делает.

Переменная Sass живёт до сборки, переменная CSS — на странице. Поменять значение на классе, в разметке или из скрипта можно только у второй.

Переменные, вложенность и смешение цветов CSS забрал себе. За Sass осталось три вещи. Первая — миксин с аргументами: именованный набор объявлений, который вставляют с разными значениями. Миксин выше CSS заменил бы одной переменной на класс, но в библиотеках миксины бывают на десятки строк. Вторая — цикл, который пишет правила по списку. Третья — модули @use: у каждого файла стилей свои переменные, и они не мешают другим файлам. Объявлять свои функции CSS пока почти не умеет: правило @function есть только в Chrome и Edge 139+, это limited — не рассчитывайте. А @mixin не поддерживает ни один браузер.

Sass жив: версия 1.104.1 вышла 12.09.2026, а пакет sass за неделю с 5 по 11 сентября 2026 года скачали 22 318 469 раз. Но это отдельный инструмент: нужны Node.js, установка пакета и шаг сборки. Пролог обходится одним файлом, который открывается в браузере без всего этого, поэтому Sass здесь только показан. Less, второй такой язык тех же лет, устроен по той же схеме.

Если Sass всё же встретится, не берите инструкции из старых учебников. Правило @import и глобальные функции вроде lighten() Sass объявил устаревшими в версии 1.80.0 от 17.10.2024: сегодня они печатают предупреждения, а в Dart Sass 3.0.0 их уберут. Пакет node-sass закрыт 24.07.2024, LibSass, на котором он работал, — 24.10.2025. Нынешний Sass — это пакет sass и модули @use.

Проект доски в части IV тоже обходится без Sass: имена классов там изолируют CSS-модули, а тему держат переменные — CSS-модули и переменные темы.

Сведём в одну таблицу приёмы, которые заменил сам CSS. Три строки — из этой статьи, остальные — из статей главы, и в правой колонке сказано, куда вернуться.

РаньшеТеперьГде разобрано
переменная Sass $brand--brand и var(--brand): наследуется и меняется на классевыше в этой статье
lighten() и darken() в Sasscolor-mix(in srgb, var(--brand), white 20%)выше в этой статье
вложенность Sass и имена из кусков &__titleвложенность CSS, имена классов целикомвыше в этой статье
normalize.css — чужой файл, выравнивающий браузерысвой сброс на :where() в несколько строксвой сброс стилей
content-box: ширина без внутреннего отступа и рамки* { box-sizing: border-box; } в сбросекоробка и поток
float и clearfix для колонокflex и grid; display: flow-root, чтобы блок вмещал плавающих детейтам же
ряд из inline-block, щели и отрицательные marginflex-ряд и gapflex и свободное место

Всё в средней колонке — Baseline widely: пишите. Левая колонка ещё встретится в чужом коде. float и normalize.css браузер понимает до сих пор, а записи Sass — только после сборки. Переписывать работающее ради таблицы не нужно, но новое пишут по средней колонке.

Всё в прологе работает без сборки. Файл, созданный в статье про скелет страницы и первый файл на диске, открывается двойным щелчком, и стили в <style> браузер читает как есть. Сборщик — программа, которая перед выкладкой делает из исходников готовые файлы: переводит .scss в .css, склеивает много файлов в несколько, стирает типы TypeScript.

Признаков, что без сборщика уже не обойтись, два. Первый — скрипт подключает чужой пакет по одному имени, например "date-fns", а не по пути к файлу. Браузер ищет файлы по адресу, а имя пакета — не адрес. К тому же пакет тянет за собой свои пакеты: отыскать их все в папке node_modules и свести в готовые файлы поручают сборщику. Второй — десятки файлов стилей и скриптов, разложенных по компонентам: их нужно свести в несколько готовых и не перепутать порядок.

В 2010-х такую работу делал Gulp — программа, которая по списку задач гоняла Sass, сжатие и склейку файлов. Пакет gulp жив, но за неделю с 5 по 11 сентября 2026 года его скачали 1 483 464 раза — около 1,1% от 132 485 786 загрузок сборщика Vite.

В учебнике сборщик появится постепенно. В части I статья про модули объяснит, откуда сборщики взялись и почему модулю даже на своём компьютере нужен локальный сервер, а Vite встретится в финале первого проекта, в списке того, что учить дальше. Как пакет ставят в проект командой в терминале и что с кодом делает сборщик, покажет часть III — установка TypeScript и три способа запустить .ts.

  • Пользовательское свойство --имя — обычное свойство CSS: спорит в каскаде и наследуется, а var() на каждом элементе подставляет то значение, которое пришло к этому элементу. Общие значения объявляют на :root, варианты переопределяют на классе. Baseline widely.
  • Запасное значение var(--x, 8px) срабатывает, только когда переменной нет. Значение, которое не годится свойству, превращает объявление в пустое место: свойство получает начальное значение, а наследуемое — значение родителя.
  • Вложенное правило без & — потомок, &.x — тот же элемент. Имён из кусков, как &__title, браузер не склеивает. Глубже двух уровней не вкладывают: каждый уровень добавляет вес. Baseline widely с июня 2026 года.
  • color-mix(in srgb, цвет, другой 20%) смешивает цвета прямо в браузере и работает с var(); результат прибор печатает записью color(srgb …). Baseline widely.
  • Sass компилируют в CSS, и его переменные исчезают до браузера. За ним остались миксины, циклы и модули @use; @function в CSS — limited, @mixin нет нигде. Инструкции с @import, lighten() и node-sass устарели.
  • Вместо float и clearfix теперь flex, grid и flow-root, вместо щелей inline-block — gap, вместо normalize.css — свой сброс, вместо content-box — border-box. Странице пролога сборщик не нужен; он понадобится, когда появятся пакеты по имени и десятки файлов.

1/4Внутри правила .card { … } стоит вложенное &.active { … }. Какие элементы оно оформит?

2/4В стилях :root { --gap: 12px; }, .list { --gap: 12; } и .item { margin-top: var(--gap, 8px); }. Какой margin-top у div с классом item внутри .list?

3/4Что останется в готовом .css от строки $brand: rgb(0, 0, 255); из файла .scss?

4/4Можно ли сегодня объявлять свои функции прямо в CSS правилом @function?

Бледная кнопка не краснеет

Кнопки берут фон из --pale, а --pale на корне смешивает --brand с белым пополам. Кнопка danger переопределяет --brand красным, но обе кнопки остаются бледно-синими. Объясните почему — поможет замер --pale у кнопок. Потом перенесите одну строку, чтобы вторая кнопка стала бледно-красной, а первая осталась бледно-синей.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Переменная, собранная из другой переменной, подставляет var() там, где её объявили. --pale объявлена на корне, и var(--brand) превратилась в синий ещё там: measure(".btn", "--pale") напечатает у обеих кнопок color-mix(in srgb, rgb(0, 0, 255), white 50%) — уже без var(). Вниз по наследству идёт готовый текст, и красная --brand кнопки danger в него не попадает. Перенесите объявление --pale в правило кнопки:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Теперь var(--brand) подставляется на каждой кнопке своя, и прибор напечатает .btn → background-color: color(srgb 0.5 0.5 1), color(srgb 1 0.5 0.5). Тот же вывод даст и color-mix() прямо в background-color, без --pale.

Активный пункт не синеет

Пункты меню серые, а активный должен быть синим, но правило .item.active проигрывает. Посчитайте специфичность обоих правил, затем перепишите вложенный блок так, чтобы активный пункт стал синим. Правило .item.active и разметку не трогайте, !important не используйте.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Три уровня вложенности — это селектор .menu .list .item весом (0, 3, 0), а у .item.active (0, 2, 0): спор решила специфичность, до порядка в коде дело не дошло. Лишние уровни ничего не уточняют, пункт с классом item и так найдётся. Уберите вложенность:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Прибор напечатает .item → color: rgb(128, 128, 128), rgb(0, 0, 255): (0, 1, 0) проигрывает (0, 2, 0). Сработает и два уровня, .menu { .item { … } }: у .menu .item тоже (0, 2, 0), и .item.active выиграет только тем, что стоит ниже. Первый вариант надёжнее — он не зависит от порядка правил.

Стили карточки из файла scss

Стили карточки перенесли из файла .scss как есть. Заголовок должен быть красным, а текст выполненной карточки с классом card--done — зелёным, но прибор показывает, что оба абзаца чёрные. Перепишите правила внутри .card так, чтобы замер показал красный и зелёный. Разметку не меняйте.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

В Sass &__title и &--done склеивались в имена card__title и card--done. Браузер не склеивает: для него & — целый селектор, и оба правила не нашли ни одного элемента. Имена пишут целиком:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Прибор напечатает .card p → color: rgb(255, 0, 0), rgb(0, 128, 0). Второй абзац зелёный по наследству: &.card--done красит саму карточку, а у абзаца своего цвета нет. Обойтись можно и без вложенности: три правила подряд, .card, .card__title и .card--done, дадут тот же вывод, если .card--done стоит ниже .card.