Тема 02

Тема 2. CSS: основы языка, оформление, дизайн-токены, темизация

Тема 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.

Источники стилей и порядок их применения

Стили попадают к элементу из нескольких источников, упорядоченных по приоритету:

  1. Браузерные стили по умолчанию (user-agent) — самый низкий приоритет.
  2. Пользовательские стили — стили, установленные пользователем через настройки браузера или расширения. На практике встречаются редко.
  3. Авторские стили — то, что написал разработчик: внешние таблицы, <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-е.

Блочная модель CSS: content, padding, border, margin
Блочная модель CSS

Особое поведение демонстрируют вертикальные 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-boxwidth задаёт ширину вместе с padding и border. При тех же значениях итоговая ширина останется 200 px, а на содержимое останется 160 - 20*2 - 1*2 = 118 px.
Сравнение content-box и border-box при одинаковых width, padding, border
Сравнение content-box и border-box

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, расслоённых на примитивные, семантические и компонентные. Эта структура естественно перерастает в темизацию — переключение между светлой и тёмной темой, синхронизированное с системными предпочтениями пользователя и его явным выбором.

Литература

  1. Consortium} {. W. W. Cascading Style Sheets, Level 1. — 1996, https://www.w3.org/TR/CSS1/.
  2. Lie H. W., Bos B. Cascading Style Sheets: Designing for the Web. — Addison-Wesley, 2005.
  3. Lie H. W. Cascading HTML style sheets –- a proposal. — 1994, https://www.w3.org/People/howcome/p/cascade.html.
  4. Meyer E. A. CSS Tools: Reset CSS. — 2007, https://meyerweb.com/eric/tools/css/reset/.
  5. Consortium} {. W. W. Selectors Level 4. — 2022, https://www.w3.org/TR/selectors-4/.
  6. Consortium} {. W. W. CSS Cascading and Inheritance Level 5. — 2024, https://www.w3.org/TR/css-cascade-5/.
  7. Consortium} {. W. W. CSS Box Model Module Level 3. — 2024, https://www.w3.org/TR/css-box-3/.
  8. Consortium} {. W. W. CSS Values and Units Module Level 4. — 2024, https://www.w3.org/TR/css-values-4/.
  9. Team} {. Sass Documentation. — 2024, https://sass-lang.com/documentation/.
  10. Team} {. C. Less Language Reference. — 2024, https://lesscss.org/features/.
  11. Consortium} {. W. W. CSS Custom Properties for Cascading Variables Module Level 1. — 2022, https://www.w3.org/TR/css-variables-1/.
  12. Consortium} {. W. W. Media Queries Level 5. — 2024, https://www.w3.org/TR/mediaqueries-5/.

Дополнительные материалы. Тема 2. CSS: основы языка, оформление, дизайн-токены, темизация

CSS как язык

  • MDN: CSS — официальный русскоязычный справочник: свойства, селекторы, функции с примерами. Основная точка входа при поиске любого свойства.
  • Дока: CSS — русскоязычное практическое руководство в формате коротких статей по конкретным задачам; полезно как «второе мнение» к MDN, когда нужен короткий рецепт «как сделать X».
  • Learn CSS (web.dev) — структурированный учебный курс от команды Google Chrome с интерактивными примерами, упражнениями и пошаговым разбором тем от синтаксиса до анимаций.
  • Eric Meyer's CSS Reset — исторически первый reset; читать ради контекста развития веб-разработки и понимания, от чего отталкивались последующие подходы.
  • A modern CSS reset (Josh W. Comeau) — пошаговый разбор современного компактного reset: каждая строка сопровождается объяснением, какую браузерную странность она исправляет и почему. Хорошая отправная точка для собственного «micro-reset».

Селекторы

  • MDN: CSS-селекторы — русскоязычный обзор всех видов селекторов с примерами и таблицей поддержки браузерами.
  • CSS Diner — браузерная игра-тренажёр на 32 уровня для отработки селекторов; даёт мгновенную обратную связь и закрепляет синтаксис базовых, дочерних, атрибутных и псевдо-селекторов.
  • Axiomatic CSS and Lobotomized Owls (Heydon Pickering, A List Apart) — классический разбор приёма * + * для управления вертикальными отступами; на одном примере показывает, как глубокое понимание универсального и смежного комбинаторов меняет архитектуру стилей.

Каскад, специфичность, наследование

  • MDN: каскад и наследование — русскоязычное руководство по каскаду, специфичности и наследованию; естественное продолжение соответствующего раздела темы.
  • Specifics on CSS Specificity (CSS-Tricks) — детальный разбор алгоритма расчёта специфичности с пограничными случаями, типичными ошибками понимания и таблицей весов селекторов.
  • CSS Cascade Layers (web.dev) — практический разбор @layer: что решает в больших проектах, как организовать слои, типичные паттерны организации layered-архитектуры с примерами.
  • CSS Cascade Layers Explained (OpenReplay Blog) — пошаговый туториал с примерами «до» и «после» применения @layer: как именно слои влияют на разрешение конфликтов и как мигрировать существующий код под layered-архитектуру.

Модель блока и единицы измерения

  • MDN: блочная модель — формальное описание блочной модели, её связи с box-sizing и поведения margin-collapsing.
  • Utopia: fluid responsive design — онлайн-генератор «жидкой» типографики и пространственной шкалы на основе clamp(); иллюстрирует подход к адаптивности без медиа-запросов и брейкпоинтов с подробным объяснением математики.
  • Every Layout (Heydon Pickering, Andy Bell) — книга-сайт о CSS-примитивах вёрстки (Stack, Cluster, Sidebar и т. п.); каждый «layout-примитив» сопровождается разбором, как он держится на блочной модели и единицах измерения, и почему ведёт себя именно так. Часть глав открыта бесплатно.
  • The surprising truth about pixels and accessibility (Josh W. Comeau) — разбор того, почему rem/em нельзя считать «то же самое, что px, но в относительных единицах»: как пользовательские настройки шрифта в браузере ломают вёрстку на px и почему это важно для доступности.

Препроцессоры CSS

  • Sass: руководство — официальное знакомство с основами Sass/SCSS: переменные, вложенность, миксины, импорты, функции для работы с цветами; включает примеры компиляции «до» и «после».
  • Less: getting started — официальная документация Less; компактная, охватывает все ключевые возможности языка и его интеграцию с Node.js-сборкой.
  • PostCSS: документация — описание архитектуры PostCSS (как устроен AST-обход и плагины), каталога плагинов и интеграции со сборщиками.
  • Autoprefixer (GitHub) — самый популярный PostCSS-плагин: автоматически добавляет вендорные префиксы (-webkit-, -moz-, -ms-) на основе browserslist-конфигурации проекта. README объясняет, как настройка влияет на размер итогового CSS.
  • postcss-preset-env — плагин, позволяющий писать перспективный CSS (вложенность, &-парент, новые функции) и автоматически компилировать его в синтаксис, совместимый со старыми браузерами; на главной — таблица поддерживаемых возможностей по «стадиям» спецификации.

CSS Custom Properties и дизайн-токены

  • MDN: использование CSS Custom Properties — русскоязычное руководство по объявлению, использованию, наследованию и динамической подмене переменных через JavaScript.
  • Style Dictionary (Amazon, GitHub) — практический инструмент для генерации токенов в разные форматы (CSS, iOS, Android, JSON) из единого источника. README + примеры показывают, как организовать архитектуру токенов и подключить их к проекту.
  • Material Design 3: design tokens — публичная документация системы токенов Material 3: подробно расписана трёхуровневая архитектура primitive → semantic → component с реальными именованиями и взаимосвязями. Полезный референс при проектировании собственной системы.

Темизация интерфейса

  • prefers-color-scheme: hello darkness, my old friend (web.dev) — практическое руководство по тёмной теме через одноимённую медиа-фичу: основы, интеграция с пользовательским переключателем, доступность.
  • Building a theme switch component (web.dev, Adam Argyle) — пошаговый разбор создания компонента-переключателя темы: разметка, синхронизация с системой, сохранение в localStorage, доступная разметка. Часть серии GUI Challenges.
  • MDN: prefers-reduced-motion — справочник по медиа-фиче для пользователей, отключивших анимации в системе; важный аспект доступности темизации и переходов между темами.
  • MDN: light-dark() — современная CSS-функция, позволяющая задавать пары значений для светлой и тёмной темы прямо в объявлении и тем самым избегать дублирования блоков переменных.
  • Avoiding the dark mode flash (Josh W. Comeau) — детальный разбор «вспышки» неправильной темы при загрузке страницы (FART, flash of inaccurate theme): почему она возникает, как её избежать через blocking-script на ранней стадии, какие компромиссы есть у разных решений.

Практическое занятие 2. Стилизация дашборда «Личный кабинет магистранта»: дизайн-токены и темизация

Объём: 2 академических часа

Раздел курса: тема 2 «CSS: основы языка, оформление, дизайн-токены, темизация»

Введение

На предыдущем занятии вы собрали семантичный HTML-каркас дашборда и увидели, как он выглядит без авторских стилей — с одним лишь user-agent stylesheet. Это занятие надстраивает над разметкой второй слой клиентской вёрстки — CSS. Вместо того чтобы стилизовать страницу хардкод-значениями, мы сразу начинаем с дизайн-токенов: трёхуровневой системы CSS Custom Properties (primitive → semantic → component), на которой потом строится темизация. К концу занятия дашборд приобретает законченный визуальный вид, переключается между светлой и тёмной темой и синхронизирует выбор пользователя с системными настройками и localStorage.

Принципы темы 2 — каскад, специфичность, блочная модель, единицы измерения, переменные — здесь применяются на практике: каждое решение в коде должно опираться на знакомое из лекции понятие, а не на «попробовал — получилось».

Цель работы

Освоить стилизацию через дизайн-токены и реализовать переключение темы на материале референсного дашборда «Личный кабинет магистранта».

После выполнения работы магистрант сможет:

  • подключать внешнюю таблицу стилей и писать собственный «micro-reset» с осознанным набором правил;
  • управлять расчётом размеров элементов через box-sizing: border-box, обосновывать выбор единиц rem/em/%/ch/dvh для конкретных свойств;
  • декларировать дизайн-токены тремя уровнями (primitive → semantic → component) и применять их вместо хардкод-значений;
  • использовать каскадируемость CSS Custom Properties для переключения темы через атрибут на корневом элементе, не дублируя правила компонентов;
  • синхронизировать ручное переключение с системной настройкой prefers-color-scheme и сохранять выбор в localStorage, не допуская «вспышки» неправильной темы при загрузке;
  • проверять контрастность светлой и тёмной темы через DevTools и осознанно принимать или исправлять её нарушения.

Теоретический минимум

Часть материала уже разобрана в теме 2 и здесь сводится к коротким отсылкам; остальное (новые инструменты DevTools и приём против «вспышки» темы) вводится прямо в задании.

Из лекционного материала

  • Способы подключения CSS, синтаксис правил и комментариев — см. учебное пособие, тема 2, раздел «CSS как язык».
  • Селекторы (тег / класс / идентификатор / атрибут, псевдоклассы, комбинаторы) — там же, раздел «Селекторы».
  • Каскад, специфичность, наследование, !important как исключение — раздел «Каскад, специфичность, наследование».
  • Блочная модель и box-sizing, единицы rem/em/%/ch/dvh, функция clamp() — раздел «Модель блока и единицы измерения».
  • CSS Custom Properties и трёхуровневая иерархия токенов (primitive / semantic / component), каскадируемость переменных — раздел «CSS Custom Properties и дизайн-токены».
  • Автоматическая тема через prefers-color-scheme и ручная через атрибут на <html> с синхронизацией в localStorage — раздел «Темизация интерфейса».

Инструментарий, вводимый на занятии

DevTools: панель Styles и панель Computed. Кликните любой элемент во вкладке Elements — справа появятся две связанные панели. Styles показывает все CSS-правила, применённые к элементу, упорядоченные по специфичности и происхождению (включая user-agent stylesheet); зачёркнутые объявления — те, что проиграли в каскаде. Computed показывает итоговые вычисленные значения свойств после разрешения каскада, наследования и единиц измерения — здесь, например, 1rem уже превратился в конкретный 16px. Эти две панели — основной инструмент диагностики «почему свойство не применилось» или «откуда взялся этот отступ».

DevTools: эмуляция тем и контраст. В DevTools есть режим эмуляции пользовательских предпочтений: Cmd/Ctrl+Shift+P → Show Rendering → Emulate CSS media feature prefers-color-scheme. Это позволяет проверять обе темы, не меняя настройки операционной системы. Там же — Emulate CSS media feature prefers-reduced-motion. Проверка контраста встроена в color picker: открыть Styles → клик по образцу цвета, в выпадающем окне будет показан коэффициент контраста (Contrast ratio) с фоном и две красные/зелёные галочки: AA (≥ 4.5:1 для основного текста) и AAA (≥ 7:1).

localStorage и blocking-script. localStorage — хранилище пар «ключ → строка», доступное в браузере между сессиями (подробнее о Storage API — в теме 7). Чтение: localStorage.getItem('theme'), запись: localStorage.setItem('theme', 'dark'). Чтобы тема, выбранная пользователем в прошлый визит, применялась до отрисовки страницы, скрипт чтения помещается синхронно в <head> сразу после <meta> — без defer и async. Иначе возникает «вспышка» неправильной темы (FART, flash of inaccurate theme): страница успевает отрисоваться в светлой теме, прежде чем JavaScript меняет атрибут на <html>. Подробный разбор приёма — в дополнительных материалах темы.

Перечень оснащения

  • VS Code 1.90+ либо аналогичный редактор.
  • Chrome 120+ или Firefox 120+ с включёнными DevTools.
  • Установленный git, доступ к GitLab кафедры (см. общие правила выполнения работ практикума).
  • Решение практического занятия 1lab-01/index.html из вашего проекта; копируется в lab-02/ как стартовая точка.
  • Макет дашборда в Figma: aidt-mag-frontend-sample, фреймы dashboard-student-light и dashboard-student-dark. В макете заданы значения цветов, скруглений, отступов и типографических размеров — они служат источником значений для primitive-токенов.
  • Бриф и доменная модель референсного проекта — pz-env/brief.md рядом с текстом занятия (подсказки для подписей, бейджей, состояний).

Порядок выполнения работы

Работа состоит из четырёх частей. Первые две — техническая подготовка стилевой системы; третья — применение токенов к разметке; четвёртая — темизация. По итогам каждой части магистрант фиксирует промежуточный результат коммитом в рабочую ветку.

Часть 1. Подключение CSS, micro-reset, блочная модель

Задание

  1. Скопируйте lab-01/index.html в lab-02/index.html. Это рабочий файл занятия; разметку трогать не нужно, кроме одного дополнения — подключения внешней таблицы стилей.
  2. Создайте lab-02/styles.css и подключите его в <head> через <link rel="stylesheet" href="styles.css">. Откройте страницу в браузере, убедитесь, что во вкладке Network styles.css загружается со статусом 200.
  3. Напишите собственный «micro-reset» в начале styles.css. Он должен:
    • переключить всё дерево на border-box (*, *::before, *::after { box-sizing: border-box });
    • обнулить margin у <body> и заголовков;
    • привести <img> к адаптивному поведению (max-width: 100%; height: auto; display: block);
    • задать базовый шрифт и line-height на <body>.

    Готовые сторонние reset-таблицы (normalize.css, modern reset Andy Bell или Josh Comeau) не подключайте — навык занятия именно в осознанной декларации стартовых правил. Сторонние таблицы вы можете изучить в дополнительных материалах темы.
  4. Откройте DevTools, выберите <body> во вкладке Elements, переключитесь на Computed. Сравните вычисленные значения box-sizing, margin, font-family, line-height до и после применения reset.

Результат

lab-02/index.html подключает styles.css. В файле styles.css есть короткий блок reset (10–20 строк) с комментариями, поясняющими каждое правило. На вкладке Computed DevTools видно, что reset перебил соответствующие правила user-agent stylesheet (зачёркнутые строки в Styles).

Часть 2. Декларация дизайн-токенов в три слоя

Задание

  1. Под reset-блоком в styles.css объявите блок токенов на :root. Структура — три слоя, разделённых комментариями /* === primitive === */, /* === semantic === */, /* === component === */.
  2. Primitive-слой. Выпишите «сырую» палитру и шкалы:
    • цветовые шкалы по 3–5 значений каждая: --gray-50--gray-900, --blue-500, --blue-700, --green-500, --red-500 (значения берутся из Figma-макета);
    • шкала отступов: --space-1: 4px, --space-2: 8px, --space-3: 12px, --space-4: 16px, --space-6: 24px, --space-8: 32px;
    • шкала скруглений: --radius-sm: 4px, --radius-md: 8px, --radius-lg: 14px;
    • шкала типографических размеров: --font-xs: 12px, --font-sm: 14px, --font-md: 16px, --font-lg: 20px, --font-xl: 28px.

    Имена примитивов не несут смысла применения — только описание значения.
  3. Semantic-слой. Объявите токены, описывающие роль в интерфейсе, и сошлитесь ими на примитивные через var(...):
    • --color-bg-page, --color-bg-surface, --color-bg-muted;
    • --color-text-primary, --color-text-secondary, --color-text-muted;
    • --color-border, --color-link, --color-link-hover;
    • --color-status-success, --color-status-warning, --color-status-danger;
    • --space-card-padding, --space-section-gap.
  4. Component-слой. Объявите токены, специфичные для конкретных компонентов дашборда, ссылающиеся на семантические:
    • --button-bg, --button-bg-hover, --button-text, --button-padding-y, --button-padding-x;
    • --card-bg, --card-border, --card-radius, --card-padding;
    • --metric-value-color, --badge-bg, --badge-text.

    Минимальная глубина — два уровня ссылок: компонентный токен ссылается на семантический, семантический — на примитивный. Хардкод-значения в семантическом и компонентном слое не допускаются.

Результат

В styles.css в :root лежит блок токенов с тремя ясно разграниченными слоями. На вкладке Sources → styles.css токены читаются сверху вниз как структурированный документ, а не как хаотичный список переменных. Через Inspector → Computed → Custom properties можно проверить, что значение --button-bg действительно резолвится в конечный hex-цвет.

Часть 3. Применение токенов к разметке дашборда

Задание

Стилизуйте дашборд по Figma-макету dashboard-student-light, используя только объявленные на шаге 2 токены — никакого хардкода. На каждом этапе сверяйтесь с макетом и брифом.

  1. Типографика и базовая палитра. На <body> примените --color-bg-page, --color-text-primary, базовый font-size: var(--font-md) и line-height: 1.5. На <h1>, <h2>, <h3> задайте размеры из шкалы и font-weight: 600 (Semi Bold).
  2. Layout. Расположите шапку, сайдбар и <main> через одну из доступных техник: display: grid на корневом контейнере (двухколоночная схема — сайдбар + контент, шапка через grid-row: span) или Flexbox + фиксированный сайдбар. Высота приложения — min-height: 100dvh. Layout-методы будут детально разобраны в теме 3, здесь достаточно собрать рабочий каркас.
  3. Карточки и секции. Каждая карточка KPI и каждая секция (section) — <article>/<section> с background: var(--card-bg), border: 1px solid var(--card-border), border-radius: var(--card-radius), padding: var(--card-padding). Между секциями — gap: var(--space-section-gap).
  4. Кнопки и интерактивные элементы. <button> использует компонентные токены --button-*. На :hover фон меняется на --button-bg-hover, на :focus-visible — обводка 2px solid var(--color-link). Дополнительный класс-модификатор (например, .button--primary) переопределяет --button-bg локально внутри .button--primary { … }, не дублируя правило .button.
  5. Состояния и статусы. Бейджи статусов («сдано», «просрочено», «в работе») используют --color-status-* через семантический слой. Активный пункт сайдбара (aria-current="page") — фон --color-bg-muted и левая полоса border-left: 3px solid var(--color-link).
  6. Типографические нюансы. Длина строки в текстовых блоках ограничивается через max-width: 65ch. Размеры в <h1> использует clamp() для «жидкого» поведения: font-size: clamp(var(--font-lg), 4vw, var(--font-xl)).

Хардкод-значений в стилях быть не должно — все цвета, отступы, размеры и скругления берутся из токенов. Поиском по styles.css (без блока :root) не должны находиться #-цвета и числовые значения отступов кроме нулей и 1px для бордеров.

Результат

Светлая тема дашборда в браузере выглядит близко к Figma-макету dashboard-student-light. Через Computed DevTools видно, что свойства разрешаются через цепочку токенов: компонентный → семантический → примитивный → конкретное значение. Ширина страницы остаётся читаемой и на узком (375 px) и на широком (1920 px) вьюпорте — это проверяется через DevTools Device Toolbar.

Часть 4. Темизация: ручное переключение и синхронизация

Задание

  1. Тёмная тема через атрибут. Под блоком токенов добавьте секцию [data-theme="dark"] { … }, в которой переопределите только семантический слой — компонентные токены ссылаются на семантические и подхватят изменения автоматически. Примерная подмена: --color-bg-page--gray-900, --color-text-primary--gray-50, --color-bg-surface--gray-800, --color-border--gray-700. Примитивный слой при этом не дублируется: «сырые» цвета — это шкала проекта, она одна на обе темы.
  2. Кнопка переключения. Добавьте в шапку кнопку с классом .theme-toggle и aria-label="Переключить тему". На клик она читает текущее значение document.documentElement.dataset.theme, инвертирует его ('dark''light') и записывает в localStorage под ключом 'theme'. Скрипт можно положить в конец <body> через <script src="theme.js"> или прямо инлайн в шапке.
  3. Сохранение выбора. При загрузке страницы значение из localStorage.getItem('theme') должно применяться до отрисовки. Для этого добавьте короткий синхронный скрипт прямо в <head> сразу после <meta>-тегов, без defer/async:
    <script>
      const saved = localStorage.getItem('theme');
      if (saved) document.documentElement.dataset.theme = saved;
    </script>
    
    Проверьте: при перезагрузке страницы тема не «вспыхивает» на долю секунды в неправильном цвете.
  4. Синхронизация с системой. Добавьте автоматическую тему через медиа-запрос — но так, чтобы явный пользовательский выбор её перебивал:
    @media (prefers-color-scheme: dark) {
      :root:not([data-theme]) { /* … токены тёмной темы … */ }
    }
    
    Селектор :root:not([data-theme]) срабатывает только когда пользователь ещё не выбрал тему явно. После явного выбора атрибут data-theme появляется, и медиа-запрос отключается.
  5. Проверка контраста. В DevTools для каждой пары «текст — фон» из обеих тем (основной текст на странице, текст на карточке, текст на кнопке) откройте color picker и убедитесь, что контрастность достигает AA (≥ 4.5:1) для основного текста и ≥ 3:1 для крупного. Несоответствия исправляются точечно — обычно подменой одного-двух токенов в семантическом слое.

Результат

Дашборд переключается между светлой и тёмной темой по нажатию на кнопку в шапке. Выбор сохраняется между сессиями. При первом открытии страницы (без записи в localStorage) тема соответствует системной настройке: на устройстве в тёмном режиме — тёмная, в светлом — светлая. Эмуляция в Rendering → prefers-color-scheme в DevTools подтверждает оба сценария. Контраст пар «текст — фон» в обеих темах соответствует AA. «Вспышки» неправильной темы при перезагрузке страницы нет.

Форма отчёта

Базовая структура отчёта — в общих правилах выполнения работ практикума, раздел «Состав отчёта». Специфика отчёта по этой работе:

  • решение в файле lab-02/index.html (с подключением styles.css) и lab-02/styles.css. Скрипты темизации — в <script> внутри HTML или в отдельном lab-02/theme.js;
  • в lab-02/screenshots/ — четыре скриншота: светлая и тёмная тема в полном размере (light.png, dark.png) и две выкладки DevTools с проверкой контраста (contrast-light.png, contrast-dark.png);
  • в lab-02/report.md в разделе «Ход выполнения» — ответы на четыре вопроса:
    1. Перечислите токены primitive-слоя, использованные хотя бы дважды на странице, и поясните, почему они получили общее имя в шкале (не специфичное для роли).
    2. Какие семантические токены переопределяются в [data-theme="dark"], а какие остались общими и почему?
    3. Опишите, что происходит с темой страницы в трёх ситуациях: (а) пользователь зашёл впервые в системе с тёмным режимом; (б) пользователь нажал кнопку переключения; (в) перезагрузил страницу через сутки.
    4. Какие пары «текст — фон» в вашей реализации показали наименьший контраст и как вы их исправили (или почему оставили)?
  • макеты, бриф и Figma-ссылки в проект не копируются — читаются из курсового репозитория и Figma по ссылкам из раздела «Перечень оснащения».

Контрольные вопросы

  1. Почему почти все современные проекты переключают всё дерево на box-sizing: border-box? Приведите пример вёрстки, в которой content-box дал бы менее предсказуемое поведение.
  2. В чём смысл расслоения токенов на примитивные, семантические и компонентные? Что произойдёт с проектом без семантического слоя при смене основного фирменного цвета?
  3. Почему при переключении темы достаточно переопределить только семантический слой, а компонентный остаётся неизменным?
  4. В чём отличие var(--name, fallback) от var(--name)? В каких случаях fallback срабатывает на практике?
  5. Почему [data-theme="dark"] имеет более высокую специфичность, чем :root, и как это связано с приёмом «явный выбор перебивает медиа-запрос»? Сравните с применением правила :root:not([data-theme]) в @media (prefers-color-scheme: dark).
  6. Что такое «вспышка темы» (FART) и почему она лечится именно синхронным <script> в <head>, а не обычным скриптом перед закрывающим </body>? Объясните в терминах последовательности парсинга и отрисовки.
  7. CSS Custom Properties объявлены на :root, но локально переопределяются в селекторе .section-promo { --button-bg: #c00 }. Какое значение получит <button> внутри .section-promo, и почему это считается каскадом, а не специфичностью?
  8. Почему медиа-запрос @media (prefers-color-scheme: dark) нельзя написать как @media (var(--theme))? В каких уровнях CSS работают переменные, а в каких — нет?
  9. Почему длина строки в текстовых блоках ограничивается единицей ch, а не px? Чем это связано с типографической читаемостью?
  10. Какие проверки контраста встроены в DevTools браузера, и какие пороги соответствуют уровням WCAG AA и AAA для основного текста? В каких ситуациях достаточно AA, в каких имеет смысл подняться до AAA?