Тема 2. CSS: основы языка, оформление, дизайн-токены, темизация
В прошлой теме мы условились, что HTML описывает структуру и смысл документа, не его внешний вид. Тогда возникает вопрос: чем же управлять оформлением — цветом, шрифтом, отступами, расположением блоков? Ответ — CSS, второй язык клиентской вёрстки. Мы рассмотрим CSS постепенно: от его роли и базового синтаксиса к селекторам и каскаду, затем к блочной модели и единицам измерения, и завершим темой, через которую разработчики управляют визуальной системой современных интерфейсов: дизайн-токенами и темизацией.
CSS как язык
Назначение CSS: отделение оформления от структуры
Каскадные таблицы стилей (Cascading Style Sheets, CSS) — язык, описывающий, как должен отображаться контент, размеченный с помощью HTML 12. CSS позволяет задавать цвета, шрифты, размеры, отступы, расположение элементов, анимации, реакцию на действия пользователя и адаптацию под размер экрана.
Ключевая идея CSS — разделение содержания и оформления. Один и тот же HTML-документ может выглядеть совершенно по-разному в зависимости от подключённых стилей: на это свойство опирается, например, темизация, печать страницы, адаптация под мобильные устройства и доступность для пользователей с особыми потребностями. Подход «разметка отдельно, стилизация отдельно» сегодня воспринимается как очевидный, но он отнюдь не был исходным состоянием веба — в эпоху HTML 4 повсеместной была практика прямого управления оформлением через атрибуты тегов (bgcolor, align, font) и табличную вёрстку. Со временем стало ясно, что такая смесь делает разметку нечитаемой, поддержку — дорогой, а попытки изменить дизайн — болезненными. CSS появился именно как инструмент разделения этих забот 3.
Синтаксис правила: селектор, блок объявлений, свойство и значение
CSS работает на основе правил. Каждое правило состоит из селектора и блока объявлений, разделённых фигурными скобками:
p {
color: #333;
font-size: 16px;
line-height: 1.5;
}
Здесь p — селектор, указывающий, к каким HTML-элементам применить правило (в данном случае ко всем <p>); внутри фигурных скобок размещается блок объявлений; каждое объявление состоит из имени свойства (color, font-size, line-height), двоеточия и значения; объявления разделяются точкой с запятой. Пробелы и переносы строк не значимы — приведённый формат принят как соглашение для читаемости, но запись p{color:#333;font-size:16px} валидна и приведёт к тому же результату.
Свойств в CSS — несколько сотен; их полный перечень с правилами применения и значениями приводится в спецификации W3C/WHATWG, а на практике удобнее обращаться к справочникам MDN или Доке. Запоминать каждое свойство наизусть не требуется — достаточно понимать систему типов значений (длины, цвета, ключевые слова, функции) и принципы каскада, о которых пойдёт речь ниже.
Комментарии
В CSS используется единственный синтаксис комментария — /* ... */. Однострочной формы вроде // (как в JavaScript) в нативном CSS нет; комментарии тоже не вкладываются — первая встреченная пара */ закрывает текущий комментарий, и всё, что после, читается как обычные правила. Внутри блока объявлений комментарием удобно временно отключать отдельные строки:
.button {
background: #06c;
/* color: white; */
padding: 8px 16px;
}
Способы подключения стилей
Чтобы CSS-правила начали действовать, их нужно подключить к HTML-документу. Существуют три способа.
Внешний CSS-файл — основной и предпочтительный способ. Стили записываются в отдельный файл с расширением .css и подключаются через элемент <link> в <head>:
<head>
<link rel="stylesheet" href="styles.css">
</head>
Атрибут rel="stylesheet" сообщает браузеру, что подключаемый файл — таблица стилей; в href указывается путь к файлу. Вынесение стилей в отдельный файл даёт два важных преимущества: один и тот же файл стилей можно подключить к нескольким страницам, а браузер кеширует загруженный файл, ускоряя последующие переходы.
Элемент <style> позволяет встроить стили прямо в HTML-документ, обычно — в <head>:
<head>
<style>
p { color: #333; }
</style>
</head>
Такая запись оправдана для критических стилей, которые должны применяться немедленно, до загрузки внешних таблиц, либо для одностраничных документов, у которых нет повторного использования стилей. В обычной практике её избегают: смешение разметки и стилизации затрудняет сопровождение.
Инлайн-атрибут style позволяет задать стили прямо на элементе:
<p style="color: #333; font-size: 16px;">Абзац с инлайн-стилями.</p>
Это самая жёсткая привязка стилизации к разметке: правила нельзя переиспользовать, селекторы вообще не работают, специфичность таких объявлений максимальна. Инлайн-style уместен в редких случаях — например, когда значение свойства вычисляется на лету в JavaScript и применяется к конкретному элементу. В остальных ситуациях разумно держаться внешних стилей.
Помимо этих трёх способов внутри одного CSS-файла можно подключить другой — директивой @import:
@import url('typography.css');
Так удобно разбивать большую стилевую базу на тематические файлы. На практике, однако, чаще подключают каждый файл собственным <link>-ом: браузер загружает их параллельно, тогда как @import-ы выстраиваются в последовательную цепочку и задерживают отрисовку. Поэтому @import чаще встречается для специальных случаев — например, для подключения шрифтов с CDN.
Браузерные стили по умолчанию (user-agent stylesheet) и подходы к их сбросу
Браузерные стили по умолчанию (user-agent stylesheet) упоминались в теме 1: именно они объясняют, почему даже без авторских стилей <h1> крупнее <p>, ссылки <a> синие и подчёркнуты, а кнопки имеют рамку. Это встроенный в каждый браузер минимальный набор правил, обеспечивающий читаемость немаркированного документа.
Проблема в том, что user-agent stylesheet несколько различается между браузерами. Это означает: даже одинаковый HTML без собственных стилей может выглядеть в Chrome, Safari и Firefox чуть по-разному — разные отступы у списков, разный размер кнопок, разные рамки полей ввода. Для проектов, которым важен предсказуемый и одинаковый внешний вид, это нежелательно.
Исторически сложились два подхода к выравниванию стартовой точки: reset и normalize 4. Reset обнуляет почти все встроенные стили: убирает отступы, размеры шрифтов, оформление списков, превращая разметку в визуально нейтральный полуфабрикат, поверх которого автор пишет стилизацию с нуля. Normalize поступает мягче: оставляет осмысленные браузерные стили и лишь сглаживает различия между браузерами там, где они нелогичны или нарушают консистентность.
Селекторы
CSS-селекторы — выражения, определяющие, к каким элементам страницы применить блок объявлений 5. Это, пожалуй, самая богатая часть языка: за тридцать лет в CSS накопилось несколько десятков видов селекторов и комбинаторов. Рассмотрим их по группам — от базовых к продвинутым.
Прежде чем переходить к самим селекторам, договоримся о терминологии родственных отношений между элементами. HTML-документ образует иерархию вложенных элементов; в этой иерархии любые два элемента находятся в одном из таких отношений. Если элемент B непосредственно вложен в элемент A, то A для B — родительский элемент, а B для A — дочерний элемент. Все элементы, лежащие внутри A на любой глубине вложенности, — его элементы-потомки; сам A для них — элемент-предок. Элементы, имеющие общий родительский элемент, называются соседними. На этих четырёх понятиях держится почти весь язык селекторов.
Селекторы по тегу, классу, идентификатору, атрибуту
Селектор по тегу применяет правило ко всем элементам определённого типа:
p { color: blue; }
Это правило окрасит в синий все <p> в документе. Селекторы по тегу удобны для базовой типографики и сброса браузерных стилей, но в большинстве компонентных интерфейсов используются вместе с другими видами селекторов, чтобы избежать слишком общего охвата.
Селектор по идентификатору начинается с символа # и выбирает элемент, у которого атрибут id совпадает с указанным значением:
<p id="lead">Лид-абзац статьи.</p>
#lead { font-size: 24px; }
В рамках одного HTML-документа значение id должно быть уникальным — это требование стандарта HTML, а не CSS. На практике id-селекторы используются редко: они сильнее по специфичности (см. ниже), плохо переиспользуются и затрудняют сопровождение. Большинство современных проектов держат стилизацию на классах, оставляя id за якорными ссылками и привязками <label for>.
Селектор по классу начинается с точки и выбирает все элементы с указанным классом:
<p class="callout">Важное замечание.</p>
<p class="callout">Ещё одно.</p>
.callout {
background: #fffbe6;
padding: 12px;
}
Класс — основной механизм навешивания стилей в современной разработке. У одного элемента может быть несколько классов через пробел (class="button primary"); один класс может применяться к любому количеству элементов.
Селектор по атрибуту позволяет выбирать элементы по наличию указанного HTML-атрибута или по его значению. Базовая форма — [имя]:
[disabled] { opacity: 0.5; }
Это правило подействует на любой элемент, у которого в разметке есть атрибут disabled, независимо от его значения.
Если нужно проверить и значение, добавляется оператор сравнения. Простейший — точное равенство:
[type="submit"] { background: #06c; }
Помимо точного равенства CSS поддерживает несколько операторов для частичного сопоставления значения:
[href^="https://"]— значение начинается с указанной строки;[src$=".jpg"]— значение заканчивается указанной строкой;[title*="важно"]— значение содержит указанную подстроку;[class~="highlight"]— в значении есть словоhighlightцеликом (для атрибутов, хранящих список слов через пробел:class,rel);[lang|="en"]— значение равноenлибо начинается сen-(формат языковых кодов:en,en-US,en-GB).
Атрибутные селекторы особенно полезны при стилизации форм ([type="email"], [required]), внешних ссылок ([href^="https://"]), элементов с состоянием ([aria-pressed="true"]) — везде, где состояние уже выражено в разметке через атрибут и заводить под него отдельный класс нерационально.
Универсальный селектор * соответствует любому элементу:
* { box-sizing: border-box; }
Универсальный селектор уместен в стартовых установках вроде сброса box-sizing (см. ниже), но в прикладной стилизации его применение лучше ограничить — массовое применение правил ко всем элементам страницы может незаметно влиять на производительность и приводить к нежелательным побочным эффектам.
Псевдоклассы и псевдоэлементы
Все рассмотренные выше селекторы опираются только на то, что прямо записано в разметке: имя тега, класс, идентификатор, значение атрибута. Но у элемента бывают признаки, которых в HTML нет: динамические состояния (наведён курсором, получил фокус, заблокирован, отмечен) и позиционные характеристики, не выраженные атрибутами («первый дочерний среди соседей», «каждый чётный»). Чтобы выбирать элементы по таким признакам, в CSS введены псевдоклассы. Имя псевдокласса записывается после селектора через одно двоеточие — как будто к элементу присоединили дополнительный класс. Отсюда и приставка «псевдо»: в разметке этого «класса» нет, но селектор работает так, словно он там есть.
a:hover { text-decoration: underline; } /* курсор над элементом */
a:focus { outline: 2px solid #06c; } /* элемент получил фокус */
input:checked + label { font-weight: 600; } /* чекбокс отмечен */
input:disabled { opacity: 0.5; } /* элемент заблокирован */
li:first-child { margin-top: 0; } /* первый дочерний */
li:last-child { margin-bottom: 0; } /* последний дочерний */
li:nth-child(odd) { background: #f5f5f5; } /* нечётные дочерние */
Псевдоклассы состояния (:hover, :focus, :active, :checked, :disabled) — основа интерактивности на уровне CSS: огромное количество поведений интерфейса описывается с их помощью. Структурные псевдоклассы (:first-child, :last-child, :nth-child, :empty) выбирают элементы по их положению в иерархии разметки и количеству соседних элементов.
Если псевдокласс — это виртуальный признак на уже существующем элементе, то псевдоэлемент — это виртуальный элемент, которого в разметке вовсе нет, но CSS позволяет его адресовать и стилизовать. К таким «несуществующим» частям относятся первая строка абзаца, первая буква, выделенный пользователем фрагмент текста, а также искусственно вставленное содержимое до или после элемента. Имя псевдоэлемента пишется через два двоеточия — этим он синтаксически отличается от псевдокласса:
p::first-line { font-weight: 600; } /* первая строка абзаца */
p::first-letter { font-size: 200%; } /* буквица */
p::selection { background: #ffe066; } /* выделенный текст */
.quote::before { content: "« "; } /* содержимое перед элементом */
.quote::after { content: " »"; } /* содержимое после */
Особо стоит выделить ::before и ::after со свойством content — они генерируют псевдоузлы перед и после содержимого элемента и широко используются для декоративных элементов, кавычек, иконок, маркеров списков, разделителей. Эти псевдоузлы не присутствуют в HTML, недоступны JavaScript-скриптам и в большинстве случаев не воспринимаются скринридерами как контент — что и удобно, когда мы добавляем чисто декоративные детали.
Исторически псевдоэлементы записывались с одним двоеточием, как и псевдоклассы (:before, :after), и старая запись по сей день поддерживается ради совместимости — но в новом коде следует использовать ::before и ::after, чтобы синтаксически отличать псевдоэлементы от псевдоклассов.
Комбинаторы: потомки, дочерние, соседние
Комбинаторы выражают отношения между элементами в дереве документа. Их четыре.
Потомковый комбинатор (пробел) выбирает все элементы, вложенные в указанный родительский элемент на любом уровне:
<div>
<p>Прямой потомок div.</p>
<span>
<p>Потомок div через span.</p>
</span>
</div>
div p { font-size: 18px; } /* оба <p> получат правило */
Дочерний комбинатор (>) выбирает только прямых потомков:
div > p { color: blue; } /* только первый <p>, второй нет */
Смежный комбинатор (+) выбирает элемент, идущий непосредственно после указанного, на том же уровне вложенности:
<h2>Заголовок</h2>
<p>Этот абзац получит margin-top: 0.</p>
<p>А этот нет — он не следует сразу за h2.</p>
h2 + p { margin-top: 0; }
Общий соседний комбинатор (~) выбирает все элементы, идущие после указанного на том же уровне:
h2 ~ p { color: gray; } /* все <p>, идущие после h2, серые */
Один селектор может объединять несколько комбинаторов в цепочку: nav ul > li:first-child a — все ссылки внутри первого пункта списка, который является прямым потомком <ul> внутри <nav>. Длинные цепочки селекторов работают, но их сложно читать и поддерживать; в современной практике их обычно удерживают на двух-трёх уровнях, опираясь на классы.
Современные псевдоклассы: :is, :where, :has, :focus-visible, :focus-within
За последние годы CSS пополнился несколькими функциональными псевдоклассами, заметно меняющими привычные приёмы.
:is(...) принимает список селекторов и выбирает элементы, подходящие хотя бы под один из них. Это сокращает повторение:
/* было */
header h1, main h1, aside h1 { font-family: serif; }
/* стало */
:is(header, main, aside) h1 { font-family: serif; }
У :is есть нюанс, связанный со специфичностью — условным «весом» селектора, по которому каскад выбирает между конкурирующими правилами (формальное определение даётся ниже, в разделе «Каскад, специфичность, наследование»). Специфичность :is равна специфичности самого «тяжёлого» из перечисленных селекторов, что иногда приводит к неожиданному поведению.
:where(...) работает так же, как :is, но имеет нулевую специфичность — то есть в каскаде ведёт себя так, словно его нет, и не повышает приоритет правила. Это делает его идеальным для написания низкоприоритетных «базовых» стилей, которые легко переопределить дальше:
:where(article, aside) p { line-height: 1.6; }
/* такое правило перебьётся любым более специфичным */
:has(...) — родительский селектор, долго ожидавшийся в CSS. Он выбирает элемент, если внутри него есть совпадающий с указанным селектор:
article:has(img) { padding-block: 24px; } /* статья содержит картинку */
form:has(input:invalid) button { opacity: 0.5; } /* в форме есть некорректное поле */
До :has подобные сценарии решались только с помощью языка JavaScript. Поддержка в браузерах появилась относительно недавно, и :has уже стал частью повседневного инструментария.
:focus-visible срабатывает только тогда, когда фокус получен через клавиатуру (Tab, стрелки), но не через клик мышью. Это позволяет показывать чёткую видимую обводку фокуса для пользователей, ориентирующихся клавиатурой, и не показывать её при кликах мышью, где она часто воспринимается как визуальный шум:
button:focus-visible { outline: 2px solid #06c; }
:focus-within срабатывает на элементе, если фокус находится внутри него или на нём самом — удобно для подсветки контейнера активной формы или раскрытого пункта меню:
form:focus-within { background: #f9fbff; }
Эти псевдоклассы аккуратно закрывают сценарии, ради которых раньше приходилось писать вспомогательный JavaScript-код, и заметно упрощают доступную интерактивность.
Каскад, специфичность, наследование
Слово «cascading» в названии CSS не случайно: к одному и тому же элементу может относиться несколько правил из разных источников, и язык должен решать, какое из них применить. Этот процесс — каскад — устроен предсказуемо, но требует понимания 6.
Источники стилей и порядок их применения
Стили попадают к элементу из нескольких источников, упорядоченных по приоритету:
- Браузерные стили по умолчанию (user-agent) — самый низкий приоритет.
- Пользовательские стили — стили, установленные пользователем через настройки браузера или расширения. На практике встречаются редко.
- Авторские стили — то, что написал разработчик: внешние таблицы,
<style>, инлайн-style,@import-ы.
Внутри каждой группы порядок дополнительно определяется тремя факторами: происхождением правила (!important повышает приоритет), его специфичностью и порядком следования в файле (более позднее правило перебивает более раннее при равной специфичности).
Помимо этих базовых механизмов, в современный CSS добавлены каскадные слои (@layer) — способ автору явно задать собственную иерархию приоритетов между группами правил, не полагаясь на хитросплетения специфичности 6. Это инструмент крупных проектов и дизайн-систем; в простой стилизации без него легко обходятся.
Вычисление специфичности селекторов и типовые ошибки интерпретации
Когда несколько правил из одного источника применимы к элементу и задают разные значения одного свойства, побеждает правило с большей специфичностью. Специфичность — целочисленный «вес» селектора, вычисляемый как тройка чисел (a, b, c):
a— количество селекторов по идентификатору (#id);b— количество селекторов по классу, атрибуту и псевдоклассу (.btn,[type="text"],:hover);c— количество селекторов по тегу и псевдоэлементу (p,::before).
Сравниваются тройки лексикографически: (1, 0, 0) сильнее (0, 9, 9). Из этого следуют типичные наблюдения:
- селектор по идентификатору всегда сильнее любой комбинации классов и тегов;
- инлайн-
style="..."имеет ещё более высокий приоритет, эквивалентный тысячам идентификаторов; победить его можно только!important; - универсальный селектор
*, комбинаторы (>,+,~) и псевдокласс:where(...)не вносят вклад в специфичность; :is(...)и:not(...)имеют специфичность, равную самому «тяжёлому» из перечисленных в них селекторов.
Типичная ошибка — пытаться «победить» нежелательный стиль, добавляя ещё одно одинаковое правило ниже в файле. Если у конкурирующего правила выше специфичность, порядок не поможет. Правильное решение — либо повысить специфичность вашего селектора, либо понизить специфичность конкурента (часто переписав его с использованием :where), либо реструктурировать стили, чтобы конкурента вовсе не возникало.
Наследование как ортогональный каскаду механизм
Помимо каскада, в CSS работает наследование: некоторые свойства, заданные на родительском элементе, передаются всем потомкам, если у тех нет собственного значения. Наследуются преимущественно типографические свойства: color, font-family, font-size, line-height, text-align. Не наследуются свойства, относящиеся к собственному оформлению элемента: background, border, padding, margin, width, height.
Какие свойства наследуются, а какие нет — определяется спецификацией; узнать это для конкретного свойства всегда можно в его справочнике. Принудительно изменить поведение можно ключевыми словами inherit (взять значение от родительского элемента), initial (вернуть к начальному значению из спецификации), unset (либо inherit, если свойство наследуемое, либо initial), revert (вернуть к user-agent значению).
Наследование — отдельный механизм, не конкурирующий с каскадом: каскад выбирает, какое значение применить к самому элементу, а наследование передаёт это значение вглубь иерархии, если у элементов-потомков своего нет.
!important как исключение, а не инструмент
Пометка !important в конце объявления повышает его приоритет, перебивая любые правила обычной специфичности из того же источника:
.button { background: gray !important; }
Эта возможность задумана как страховочный механизм: например, для пользовательских стилей в браузере, переопределяющих оформление сайта в целях доступности. В авторских стилях прибегать к ней следует только в исключительных случаях — например, при необходимости перебить стили сторонней библиотеки, которые иначе нельзя обойти.
Массовое использование !important в собственном проекте быстро превращается в гонку: одно !important тянет за собой второе, специфичность теряет смысл, и поддержка стилей становится мучительной. Если возникает соблазн поставить !important, обычно это сигнал к рефакторингу селекторов, а не к усилению давления.
Модель блока и единицы измерения
Блочная модель: содержимое, padding, border, margin
Каждый элемент на странице с точки зрения CSS — это прямоугольная коробка с четырьмя слоями: содержимое, внутренние отступы, рамка и внешние отступы. Эта схема называется блочной моделью (англ. box model) 7.
- content — собственно содержимое элемента: текст, изображение, дочерние элементы. Его размеры контролируются свойствами
widthиheight; - padding — внутренний отступ между содержимым и рамкой. Управляется свойствами
padding,padding-top,padding-right,padding-bottom,padding-left; - border — рамка вокруг padding. Управляется свойствами
border-width,border-style,border-colorили составнымborder; - margin — внешний отступ между рамкой элемента и соседними элементами. Управляется аналогичными
margin-свойствами.
Понимание блочной модели — основа всех расчётов вёрстки. Когда два соседних блока «не сходятся» по ширине, или элемент занимает больше места, чем кажется по width, объяснение почти всегда найдётся в padding-е, border-е или margin-е.
Особое поведение демонстрируют вертикальные margin-ы соседних блочных элементов — они схлопываются (англ. margin collapsing): если один блок имеет margin-bottom: 20px, а следующий за ним — margin-top: 30px, итоговый промежуток между ними будет 30 px, а не 50. Это поведение происходит из исторической логики работы с печатным текстом и часто становится источником недоумения; именно поэтому многие современные практики предпочитают задавать вертикальные интервалы только с одной стороны (например, всегда margin-top).
box-sizing: content-box против border-box
Свойство box-sizing определяет, как трактуются width и height элемента:
content-box(значение по умолчанию) —widthзадаёт ширину только содержимого, padding и border добавляются сверху. Еслиwidth: 160px; padding: 20px; border: 1px solid;, итоговая ширина элемента —160 + 20*2 + 1*2 = 202 px;border-box—widthзадаёт ширину вместе с padding и border. При тех же значениях итоговая ширина останется200 px, а на содержимое останется160 - 20*2 - 1*2 = 118 px.
border-box предсказуемее в большинстве сценариев: вы задаёте «коробку шириной 200 пикселей» — и она ровно такой и оказывается. Поэтому почти все современные проекты переключают всё дерево на border-box универсальным правилом в начале таблицы стилей:
*, *::before, *::after {
box-sizing: border-box;
}
Это компактное правило экономит часы отладки и стало де-факто стандартом первой строки CSS-проекта.
Абсолютные и относительные единицы: px, em, rem, %
CSS поддерживает много единиц измерения, разделяемых на абсолютные и относительные 8.
px (пиксели) — основная абсолютная единица. В современных браузерах 1 px не равен физическому пикселю экрана: это логическая единица, привязанная к плотности экрана и масштабу. Для большинства практических задач px достаточно предсказуем, чтобы использовать его как «опору».
em — относительная единица: 1em равен значению font-size текущего элемента. Если у <p> задан font-size: 16px, то 1em = 16px для свойств этого <p>. em каскадируется и умножается: вложенные элементы наследуют размер от родительского элемента, и em в их свойствах считается уже от наследованного значения. Это делает em мощным, но требующим аккуратности — при глубокой вложенности легко получить непредсказуемые размеры.
rem (root em) — то же, что и em, но всегда привязан к font-size корневого элемента <html>, независимо от вложенности. Это даёт предсказуемость em без её каскадных сюрпризов и потому стал основной единицей в современной типографике. Стандартное соглашение: ставят font-size: 16px на <html> (или оставляют дефолтное значение браузера, обычно те же 16 px) и далее везде используют rem. Тогда 1rem = 16px, и при изменении базового размера на <html> пропорционально пересчитывается весь интерфейс.
% — процент. Единица «контекстная»: процент от чего — зависит от свойства. Для width — процент от ширины родительского элемента; для font-size — процент от font-size родительского элемента; для line-height — процент от собственного font-size. Полезна там, где нужна привязка к контейнеру, а не к фиксированной величине.
Современные единицы и функции: ch, dvh, svw, clamp, min, max
К базовому набору единиц современный CSS добавил несколько практически ценных вариантов.
ch — ширина символа «0» в текущем шрифте. Идеален для задач с полем ширины «столько-то символов»: например, ограничить максимальную ширину абзаца как max-width: 70ch, чтобы строка вмещала примерно 70 знаков и оставалась читаемой.
Единицы вьюпорта. Вьюпортом (англ. viewport) в вебе называют видимую область окна браузера, в которой отображается страница; её размер меняется при изменении размеров окна, повороте устройства или появлении/скрытии браузерных панелей. На вьюпорт опираются единицы vw и vh — соответственно 1 % ширины и 1 % высоты этой области. Долгое время на мобильных устройствах с ними была неприятность: панели браузера то появляются, то скрываются, и 100vh мог оказываться больше реального видимого окна, отрезая контент. Чтобы это исправить, добавили dvh, svh, lvh (dynamic, small, large viewport height) — соответственно текущая, минимальная и максимальная высота вьюпорта при изменчивых панелях. Аналогично — dvw, svw, lvw. На сегодня для полноэкранных секций 100dvh — корректная замена 100vh.
Функции min, max, clamp позволяют выражать значения, реагирующие на контекст:
.container {
width: min(100%, 1200px); /* меньшее из двух: либо 100% контейнера, либо 1200 px */
}
.title {
font-size: clamp(1.5rem, 4vw, 3rem); /* плавный размер: между 1.5rem и 3rem, по 4% от ширины */
}
clamp(min, preferred, max) особенно ценен: он задаёт «жидкое» значение, плавно меняющееся в заданном диапазоне в зависимости от размера экрана. Без него адаптация под разные ширины окна потребовала бы перечисления нескольких правил — по одному для каждого порогового размера. Этот приём — основа подхода «жидкой» типографики (англ. fluid typography): одно правило на размер шрифта работает на всех экранах, без отдельной настройки для каждого диапазона.
Препроцессоры CSS
До появления в CSS собственных переменных и других удобств разработчики долго пользовались препроцессорами (англ. CSS preprocessors) — внешними инструментами, которые принимают на вход исходник на расширенном диалекте, а на выходе дают валидный CSS, понятный браузеру. Сегодня препроцессоры по-прежнему встречаются в больших кодовых базах, поэтому представление о них необходимо хотя бы для чтения чужого кода.
Самые распространённые препроцессоры — Sass (с двумя синтаксисами: .sass без скобок и .scss, синтаксически совместимый с CSS) 9 и Less 10. Эти препроцессоры добавляют над CSS примерно один и тот же набор возможностей: переменные ($primary в Sass, @primary в Less), вложенность селекторов, миксины (переиспользуемые блоки правил), функции для работы с цветами и числами, импорт частей таблицы, ветвления и циклы.
Принципиальное отличие от CSS Custom Properties — нативных переменных самого CSS, которым посвящён следующий раздел, — заключается в моменте работы. Препроцессор запускается на этапе сборки: он читает исходник, разворачивает все конструкции и порождает обычный CSS-файл, в котором уже нет ни переменных, ни миксинов. До браузера доезжает только итог. Поэтому препроцессорные переменные могут участвовать в селекторах, медиа-запросах, расчётах на этапе компиляции — но не могут изменяться во время выполнения.
Отдельное место занимает PostCSS — формально не препроцессор, а инструмент трансформации CSS через систему плагинов. С его помощью можно реализовать часть возможностей Sass (autoprefixer, nested), а можно, наоборот, отказаться от препроцессора в пользу нативного CSS с минимальной обработкой. В современных проектах сборка часто строится именно на PostCSS-плагинах, а Sass подключается избирательно — только там, где его выразительность реально нужна.
CSS Custom Properties и дизайн-токены
Переменные CSS как способ декларации дизайн-токенов
Современный интерфейс редко строится на хаотичной палитре цветов и случайных размерах: у проекта обычно есть дизайн-система — набор согласованных значений (цветов, шрифтов, отступов, скруглений, теней), повторяющихся по всему интерфейсу. Эти значения называют дизайн-токенами.
Чтобы хранить такие значения в самой таблице стилей и переиспользовать их, в CSS существуют пользовательские свойства (англ. Custom Properties), обычно называемые CSS-переменными 11. Декларируются они с двойным дефисом в имени и используются через функцию var(...):
:root {
--color-text: #1a1a1a;
--color-accent: #06c;
--space-md: 16px;
--radius-md: 8px;
}
.button {
color: var(--color-text);
background: var(--color-accent);
padding: var(--space-md);
border-radius: var(--radius-md);
}
Декларация в :root (псевдокласс, эквивалентный корневому <html>) делает переменные доступными во всём документе. Изменив одно значение на корне, мы автоматически меняем его во всех правилах, использующих var(...) — это и есть основной механизм системной стилизации и темизации.
Семантические уровни токенов: primitive, semantic, component
В зрелых дизайн-системах токены разделяют на три уровня по смыслу.
Primitive (примитивные) — «сырая» палитра: конкретные цвета, размеры, числа. Они не несут смысла применения, только описывают значение:
--blue-500: #06c;
--blue-700: #04488f;
--gray-100: #f5f5f5;
--gray-900: #1a1a1a;
--space-1: 4px;
--space-2: 8px;
--space-3: 16px;
Semantic (семантические) — токены, описывающие роль значения в интерфейсе. Они ссылаются на примитивные:
--color-text-primary: var(--gray-900);
--color-text-muted: var(--gray-700);
--color-link: var(--blue-500);
--color-link-hover: var(--blue-700);
--space-card-padding: var(--space-3);
Component (компонентные) — токены, специфичные для конкретного компонента. Они ссылаются на семантические:
--button-bg: var(--color-link);
--button-bg-hover: var(--color-link-hover);
--button-padding-y: var(--space-2);
--button-padding-x: var(--space-3);
Такое расслоение даёт два важных свойства. Во-первых, при смене темы достаточно переопределить семантический слой — компонентам не нужно ничего знать об альтернативной палитре. Во-вторых, при изменении дизайна (скажем, замена основного синего на бирюзовый) точка изменения одна — --color-link в семантическом слое.
Каскадируемость переменных и пересчёт на лету
CSS Custom Properties подчиняются обычным правилам каскада и наследования: переменная, объявленная на родительском элементе, доступна на всех элементах-потомках, если те не переопределили её. Это позволяет менять значения локально, не дублируя само правило компонента:
:root {
--button-bg: #06c; /* по умолчанию кнопки синие */
}
.section-danger {
--button-bg: #c00; /* в «опасной» секции — красные */
}
.button {
background: var(--button-bg); /* возьмёт ближайшее значение из контекста */
}
Правило .button в проекте одно, но сама кнопка отрисовывается по-разному в зависимости от того, в какое окружение она вложена: синей по умолчанию и красной — внутри .section-danger. То же правило каскада, что и для обычных свойств, работает и для переменных.
Кроме того, CSS-переменные читаются и могут быть изменены из JavaScript — языка скриптов веб-страницы, которому посвящена тема 5:
document.documentElement.style.setProperty('--color-accent', '#e91e63');
Это даёт возможность темизации, пользовательских настроек, динамических акцентов в реальном времени без перерисовки всего CSS. Функция var(...) поддерживает значение по умолчанию: var(--color-accent, #06c) — если переменная не объявлена, использовать #06c.
Ограничения Custom Properties в сравнении с препроцессорными переменными
Существенное отличие CSS-переменных от переменных препроцессоров (Sass $var, Less @var) — в моменте их разрешения. Препроцессорные переменные подставляются на этапе сборки: после компиляции в CSS никаких переменных уже нет, остаются только конкретные значения. CSS Custom Properties живут в браузере во время выполнения — поэтому их можно менять динамически.
Из этого вытекают два практических ограничения:
- Custom Properties нельзя использовать в селекторах и медиа-запросах (
@media-правилах, отвечающих за адаптацию вёрстки под разные ширины экрана; они подробно разбираются в теме 3). Запись@media (min-width: var(--breakpoint-md))не работает: медиа-запрос разбирается до того, как переменные получают значения. То же касается селекторов —var(--my-class)в селекторе невозможна; - Тип значения не контролируется. Препроцессор может проверить, что вы используете цветовую переменную там, где ожидается цвет; CSS-переменная — просто строка, и подстановка некорректного значения тихо приведёт к невалидному CSS-объявлению, которое браузер просто проигнорирует.
В проектах с большой дизайн-системой эти два мира часто сосуществуют: препроцессор отвечает за вещи, известные на этапе сборки (брейкпоинты, утилитные миксины), а CSS Custom Properties — за всё, что должно меняться динамически или различаться по контексту (темы, локальные акценты).
Темизация интерфейса
Темизация — самый частый практический повод осваивать дизайн-токены: переключение между светлой и тёмной темой стало стандартным ожиданием от современных интерфейсов.
Автоматическая тема через prefers-color-scheme
Операционные системы дают пользователям возможность выбрать предпочитаемое оформление: светлое или тёмное. Браузер транслирует этот выбор в медиа-фичу prefers-color-scheme 12, которую можно использовать в медиа-запросах CSS:
:root {
--color-bg: #ffffff;
--color-text: #1a1a1a;
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #1a1a1a;
--color-text: #f5f5f5;
}
}
body {
background: var(--color-bg);
color: var(--color-text);
}
При таком подходе сайт сам подстраивается под предпочтения пользователя, без явного переключателя в интерфейсе. Это удачная база, но во многих проектах нужен и явный выбор — например, чтобы пользователь мог переопределить системную настройку или зафиксировать тему независимо от ОС.
Ручное переключение темы: атрибут на html, синхронизация с localStorage
Сложившийся способ ручной темизации — управлять темой через атрибут на корневом элементе <html> и завести две группы переменных:
:root {
--color-bg: #ffffff;
--color-text: #1a1a1a;
}
[data-theme="dark"] {
--color-bg: #1a1a1a;
--color-text: #f5f5f5;
}
Тогда переключение темы — это присваивание атрибуту значения, выполняемое из JavaScript:
document.documentElement.dataset.theme = 'dark';
Чтобы выбор пользователя сохранялся между визитами, его записывают в localStorage (подробнее о Storage API — в теме 7) и читают на ранней стадии загрузки страницы, ещё до показа интерфейса, чтобы избежать «вспышки» неправильной темы:
<script>
const saved = localStorage.getItem('theme');
if (saved) document.documentElement.dataset.theme = saved;
</script>
Часто две схемы — автоматическая через prefers-color-scheme и ручная через атрибут — комбинируются: атрибут перебивает медиа-запрос, и в результате пользователь получает предсказуемое поведение «по умолчанию — как в системе, при явном выборе — как выбрал».
Итоги темы
CSS — не «настройка цветов и шрифтов», а полноценный декларативный язык, в котором каждое правило — утверждение «к таким-то элементам в такой-то ситуации применяй такие-то свойства». Когда правил становится больше, чем элементов, в дело вступает каскад: система предсказуемо разрешает противоречия по источнику, специфичности и порядку. Понимание этих трёх осей превращает CSS из эзотерики в инструмент, поведение которого можно объяснять и предсказывать.
На уровне отдельного элемента CSS оперирует блочной моделью: коробкой с четырьмя слоями, размер которой зависит от значения box-sizing. На уровне всего интерфейса современные практики строятся на дизайн-токенах через CSS Custom Properties, расслоённых на примитивные, семантические и компонентные. Эта структура естественно перерастает в темизацию — переключение между светлой и тёмной темой, синхронизированное с системными предпочтениями пользователя и его явным выбором.
Литература
- Consortium} {. W. W. Cascading Style Sheets, Level 1. — 1996, https://www.w3.org/TR/CSS1/.
- Lie H. W., Bos B. Cascading Style Sheets: Designing for the Web. — Addison-Wesley, 2005.
- Lie H. W. Cascading HTML style sheets –- a proposal. — 1994, https://www.w3.org/People/howcome/p/cascade.html.
- Meyer E. A. CSS Tools: Reset CSS. — 2007, https://meyerweb.com/eric/tools/css/reset/.
- Consortium} {. W. W. Selectors Level 4. — 2022, https://www.w3.org/TR/selectors-4/.
- Consortium} {. W. W. CSS Cascading and Inheritance Level 5. — 2024, https://www.w3.org/TR/css-cascade-5/.
- Consortium} {. W. W. CSS Box Model Module Level 3. — 2024, https://www.w3.org/TR/css-box-3/.
- Consortium} {. W. W. CSS Values and Units Module Level 4. — 2024, https://www.w3.org/TR/css-values-4/.
- Team} {. Sass Documentation. — 2024, https://sass-lang.com/documentation/.
- Team} {. C. Less Language Reference. — 2024, https://lesscss.org/features/.
- Consortium} {. W. W. CSS Custom Properties for Cascading Variables Module Level 1. — 2022, https://www.w3.org/TR/css-variables-1/.
- Consortium} {. W. W. Media Queries Level 5. — 2024, https://www.w3.org/TR/mediaqueries-5/.