
Привет, Хабр!
Мне кажется, что каждый фронтендер множество раз сталкивался с ситуацией, когда CSS переставал работать так, как ожидаешь. Причём не имеет значения опыт разработчика. С одинаковой вероятностью в тупик может попасть как джун, так и опытный разработчик. Не зря же CSS стал героем такого количества мемов.
Я и сам не раз попадал в подобные ситуации. Но, несмотря на все эти сюрпризы, CSS мне по-прежнему нравится. Конечно, изменить стандарты я не могу. Зато могу рассказать о ловушках, в которые когда-то я попался. Возможно, некоторые из них окажутся неожиданными и для вас.
Давайте посмотрим, что я подготовил.
Псевдо-классы :has(), :is() или :where() неожиданно повышают специфичность
За последние несколько лет в CSS появилось немало новых псевдо-классов. Я уверен, что вы уже используете :has(), :is() или :where(). Мне интересно, а вы задумывались, как браузеры рассчитывают их специфичность?
Например, вы можете сказать, какая специфичность будет у псевдо-класса :has() в моём примере?
<body> <div class="awesome-block"> <span class="awesome-text">Я волшебный текст</span> </div> </body>
.awesome-block:has(.awesome-text, #unknown-text) { background-color: tomato; }
Для ответа на этот вопрос нужно разобраться, что такое список селекторов. Как сказано в стандарте Selectors Level 4, это список из простых и сложных селекторов, разделённых запятыми.
На ключевой вопрос авторы не отвечают. А звучит он так: «Важно ли, чтобы браузеры нашли элементы с помощью селектора?»
В итоге мы получили возможность объявлять селекторы, с помощью которых браузеры никогда не найдут элементы. Именно это я сделал, добавив селектор #unknown-text, чтобы найти несуществующий в разметке элемент.
/* специфичность селектора: 1, 1, 0 */ .awesome-block:has(.awesome-text, #unknown-text) { background-color: tomato; }
Когда элемент с объявленным свойством position со значением fixed перестанет фиксироваться
Когда мы используем значение fixed для свойства position, то ждём фиксации элемента на одном месте. Мы прибиваем его «намертво» при прокрутке страницы. Ведь в любом обучающем материале нам говорили, что это обычное поведение.
Только это не всегда так. Да, элемент с установленным свойством position со значением fixed может быть не зафиксированным.
<body> <div class="awesome-container"> <div class="awesome-fixed"></div> <div class="awesome-block"> <!-- здесь много текста --> </div> </div> </body>
.awesome-container { transform: translate3d(0, 0, 0); } .awesome-fixed { width: 200px; height: 200px; position: fixed; top: 0; left: 0; background-color: tomato; }

Как видите, я прокрутил страницу, а элемент остался на своём месте и не зафиксировался вверху вьюпорта. Что ж, давайте разбираться, почему так происходит.
Для начала нам нужно вспомнить, что у каждого элемента есть родительская область, относительно которой рассчитывается позиция дочернего элемента. В стандарте CSS Display Module Level 3 ей дали название containing block.
Когда мы объявляем свойство position со значением fixed, то такой областью обычно становится вьюпорт. Но не всегда! Есть ряд свойств, которые меняют это поведение. Например, свойство transform. Как раз его я использовал в своём примере.
В стандарте CSS Transforms Module Level 1 сказано, что если для элемента указано свойство transform со значением, отличным от none, то все его дочерние элементы с объявленным свойством position со значением fixed будут использовать его как containing block.
В нашем примере браузеры используют элемент .awesome-container как такую область. По этой причине элемент отобразился таким образом.
Свойство transform является не единственным свойством, из-за которого элемент перестаёт быть зафиксированным. Такой же результат вы получите при использовании свойства perspective, backdrop-filter или filter со всеми значениями, кроме none.
Или же свойства contain со значениями layout, paint, strict и content, а также свойство will-change со значениями transform, filter и backdrop-filter.
Есть ли разница между значениями с помощью коэффициентов или процентов для свойства line-height
Свойство line-height является одним из самых обычных свойств. Оно просто задаёт высоту строки. Какие проблемы могут возникнуть?
Я уверен, что вы знаете, что значение для этого свойства можно задать с помощью коэффициента и процентов.
<body> <div class="awesome-block"> <p>Здесь отображается бессмысленный текст, который будет очень длинным, чтобы посмотреть на расстояние между строк</p> </div> <div class="awesome-block"> <p>Здесь отображается бессмысленный текст, который будет очень длинным, чтобы посмотреть на расстояние между строк</p> </div> </body>
.awesome-block { width: 300px; font-size: 16px; } .awesome-block:nth-child(1) { line-height: 1.5; /* браузеры рассчитают 24px */ } .awesome-block:nth-child(2) { line-height: 150%; /* браузеры рассчитают 24px */ }

Сейчас визуально мы получили один и тот же результат. И так будет в подавляющем количестве случаев. По этой причине я думал, что разницы между значениями 1.5 и 150% нет. Оказывается, это не так.
Для демонстрации давайте поменяем значение свойства font-size у элементов p.
.awesome-block { width: 300px; font-size: 16px; } .awesome-block p { font-size: 10px; } .awesome-block:nth-child(1) { line-height: 1.5; } .awesome-block:nth-child(2) { line-height: 150%; }


В первом блоке браузеры вычислили значение 15px для свойства line-height. Во втором мы получили значение 24px. Магия!
Нюанс заключается в том, что при использовании процентов браузеры один раз вычисляют значение и больше не перерасчитывают его. А при коэффициенте значение рассчитывается на каждое изменение размера текста.
В чём разница между псевдо-классом :lang() и селектором по атрибуту [lang]
В CSS есть псевдо-класс :lang(). Казалось бы, зачем он добавлен, если можно использовать селектор по атрибуту [lang]? Получается, они делают одну и ту же задачу?
С одной стороны, да, но есть разница. Давайте сразу посмотрим на практике. Я буду использовать разметку, в которой у элемента div будет установлен атрибут lang, а в него будет вложен элемент p с текстом.
<body> <div lang="ru"> <p>Псевдо-класс CSS :lang() выбирает элементы, основываясь на языке, на котором они определены</p> </div> </body>
Первым способом мы объявим свойство background-color с помощью селектора по атрибуту [lang].
[lang="ru"] { background-color: tomato; }

Мы видим, что к элементу div браузер применил стили. К элементу p свойство background-color не добавилось.
Теперь перепишем селектор по атрибуту на псевдо-класс :lang().
:lang(ru) { background-color: tomato; }


В инструментах разработчика видно, что браузер применил стили сразу к элементу div и p. И это не ошибка!
Дело в том, что псевдо-класс :lang() срабатывает на всех элементах, у которых язык совпадает с тем, что указан в параметрах. Мы указали значение ru. В коде два элемента, к которым этот язык применён. Следовательно, браузеры добавили стили к ним.
При именовании пользовательских CSS-свойств вас ждёт нюанс
Давайте в конце статьи проверим вашу внимательность. Для этого я придумал пример, в котором объявляется свойство color. Скажите, пожалуйста, какого цвета будет текст?
Если вы думаете, что браузеры применят значение green, то это не так.
:root { --соlоr: green; } body { color: var(--color); }

Текст чёрный. Браузеры применили значение по умолчанию. Почему так? Окунёмся немного в теорию.
Как говорится в спецификации CSS Custom Properties for Cascading Variables Module Level 1, название пользовательского свойства — это специальный CSS-идентификатор <dashed-ident>, начинающийся с --.
Одновременно к нему применимы правила синтаксиса CSS-идентификатора <ident>, описанного в стандарте CSS Syntax Module Level 3. По правилам мы можем использовать символы [a-zA-Z0-9], дефис -, подчёркивание _ и другие ASCII-символы, если их экранировать.
Так что использование кириллицы вполне легально для пользовательского свойства. Именно она используется при именовании свойств в нашем примере. Я использую буквы и при объявлении значения. А вот далее я уже написал название полностью на латинице.
Думаете, это всё? Нет! Сейчас вас позабавлю ещё одним примером. Выше я говорил, что можно экранировать символы. Это даёт возможность использовать символ *.
body { --\*: tomato; background-color: var(--\*); }
На самом деле, если вы используете пользовательские свойства для виджета, который встраивается в неизвестную среду, то экранирование поможет вам сделать эмуляцию защищённых свойств.
body { --\*awesome-property: tomato; background-color: var(--\*awesome-property); }
Я думаю, что это сработает, потому что не представляю разработчика, который в здравом уме в своём проекте будет использовать экранирование. Мне кажется, классный трюк получится!
Заключение
Подведём итог. В этой статье мы рассмотрели:
как браузеры обрабатывают списки селекторов при использовании псевдо-классов
:is(),:where(),:has()и других;когда элемент с объявленным свойством
positionсо значениемfixedперестаёт быть зафиксированным;в чём заключается разница между псевдо-классом
:lang()и селектором по атрибуту[lang];почему пользовательские свойства могут не сработать, если использовать визуально похожие символы из разных языков;
как экранировать символы при именовании пользовательских свойств.
Коллеги, мне очень интересно узнать, с какими ловушками вы сталкивались. Расскажите, пожалуйста, в комментариях ваши истории, когда CSS преподносил вам неприятные сюрпризы.
На этом всё. Спасибо за чтение!
P. S. Помогаю больше узнать про CSS в своём ТГ‑канале CSS isn't magic. Присоединяйтесь. Как вступить, написано в профиле.
© 2026 ООО «МТ ФИНАНС»