
Привет, Хабр!
CSS часто преподносит сюрпризы, способные запутать даже опытных разработчиков. Я понимаю их раздражение. Тут всё закономерно.
Однако, несмотря на потраченные нервы, мне нравится CSS. Именно поэтому мне хочется, чтобы разработчики тратили меньше времени на борьбу с ним. С этой целью я собрал ряд не самых очевидных моментов, которые в своё время ставили в тупик меня и моих коллег.
Математические выражения и функция calc()
Я не люблю указывать единицы измерения, когда нужно объявить 0
. Однажды я столкнулся с тем, что математическая функция calc()
не работает в моём любимом стиле.
Для демонстрации давайте объявим свойство padding
и рассчитаем его значение с помощью сложения.
:root {
--ad-width: 1rem;
}
body {
padding: calc(var(--basis-body-gap, 0) + var(--ad-width)); /* здесь будет calc(0 + 1rem) */
}

Посмотрев в блочную модель, я увидел, что значение не применилось. И никаких ошибок не было. «Очень странно», — подумал я.
Обратившись к стандарту CSS Values and Units Module Level 3, я обнаружил объяснение такого поведения. Оказывается, при использовании функции calc()
для свойства, принимающего значения типа <length>
, значение 0
без указания единицы измерения не поддерживается.
Возможно, тут стоит уточнить про типы значений. Внутри функции calc()
можно использовать:
<integer>
— целые числа, состоящие из одной или нескольких цифр от 0 до 9, возможно, с предшествующим знаком+
или-
;<number>
— числа типа<integer>
, но также допускающие экспоненциальную запись;<length>
— значения, используемые для измерения расстояний, представляющие собой числа с указанием единицы измерения.
Возвращаясь к моему примеру, браузеры не смогли интерпретировать 0
в качестве значения типа <length>
. После того, как я добавил единицы измерения rem
, код заработал.
:root {
--ad-width: 1rem;
}
body {
padding: calc(var(--basis-body-gap, 0px) + var(--ad-width)); /* здесь будет calc(0px + 1rem) */
}

Стандарт также содержит некоторые неочевидные нюансы, которые важно учитывать при работе с функцией calc()
. Например, при сложении и вычитании необходимо, чтобы у всех аргументов был одинаковый тип, либо один из аргументов был типом <number>
, а другой — <integer>
.
/* правильный пример */
.awesome-block {
width: calc(1500px + 5rem);
opacity: calc(0.75 + 0.15);
z-index: calc(1E2 + 5);
}
.awesome-block {
width: calc(1500px - 2rem);
opacity: calc(0.45 - 0.15);
z-index: calc(1E2 - 1);
}
/* неправильный пример */
.awesome-block {
width: calc(1500px + 35);
height: calc(1500px + 1E1);
}
.awesome-block {
width: calc(1500px - 35);
height: calc(1500px - 1E1);
}
Перейдём теперь к умножению и делению. Здесь нужно помнить, что один из аргументов обязательно должен быть типа <integer>
или <number>
. Если используется тип <length>
для обоих аргументов, то возникнет ошибка.
Дополнительный нюанс есть при делении. Браузеры преобразуют правый аргумент в тип<number>
, если он был <integer>
.
/* правильный пример */
.awesome-block {
width: calc(15px * 1E1);
height: calc(150px * 2);
opacity: calc(0.25 * 3);
z-index: calc(1E2 * 1E1);
}
.awesome-block {
width: calc(800px / 3E1);
height: calc(200px / 2);
opacity: calc(1 / 2);
z-index: calc(1E2 / 1E1);
}
/* неправильный пример */
.awesome-block {
width: calc(1000px * 20px);
}
.awesome-block {
width: calc(6000px / 6px);
}
Свойство aspect-ratio
Я очень редко использовал свойство aspect-ratio
. Разве только добавлял его к изображениям. По этой причине думал, что у меня не должно с ним возникнуть каких-либо проблем. Но однажды из-за любопытства решил прочитать стандарт CSS Box Sizing Module Level 4 и нашёл для себя много неожиданных вещей.
Для их объяснения мы начнём с термина «предпочтительное соотношение сторон» (preferred aspect ratio). Оно нужно для расчёта автоматических значений размеров элемента. Свойство aspect-ratio
устанавливает его.
В качестве примера создадим квадрат.
<body>
<div class="awesome-box"></div>
</body>
.awesome-box {
width: 150px;
aspect-ratio: 1;
}

Браузер, используя предпочтительное соотношение сторон, рассчитал значение для свойства height
, используя значение 1
для свойства aspect-ratio
. В результате мы получили желаемый результат.
Но в разметке у элемента нет текста. Добавим его и посмотрим, как изменятся размеры.
<body>
<div class="awesome-box">
<span>CSS часто преподносит сюрпризы, способные запутать даже опытных разработчиков. Я понимаю их раздражение. Тут всё закономерно.</span>
</div>
</body>

Квадрат растянулся. На первый взгляд, это может показаться ошибкой. Однако это не так. Благодаря предпочтительному соотношению сторон, свойство aspect-ratio
учитывает различные факторы, влияющие на размеры элемента.
Контент — один из них. Если ему требуется больше места, чем предполагает заданное соотношение сторон, то браузеры нарушат значение, указанное свойством aspect-ratio
. Это, как мы увидели, произошло в нашем примере.
Перейдём к следующему случаю. Мы рассмотрим элементы, находящиеся внутри флекс-контейнера.
<body>
<div class="awesome-box">
<span>1/1</span>
</div>
<div class="awesome-box">
<span>1/2</span>
</div>
</body>
body {
display: flex;
gap: 1rem;
}
.awesome-box {
width: 150px;
aspect-ratio: 1;
}
.awesome-box:nth-child(2) {
aspect-ratio: 1 / 2;
}

По умолчанию флекс-элементы стремятся растянуться вдоль дополнительной оси. Если к ним также добавлено свойство aspect-ratio
, то мы получим неожиданный результат. Браузер применит одинаковое значение свойства aspect-ratio
для всех элементов. Оно будет выбрано как максимальное среди всех объявленных.
В нашем примере у первого флекс-элемента благодаря свойству aspect-ratio
со значением 1
размер элемента по дополнительной оси составляет 150px
. У второго флекс-элемента используется значение 1/2
, что приводит к размеру в 300px
. Поскольку 300px
больше, чем 150px
, браузер решит применить значение 1/2
ко всем флекс-элементам.
Но такой механизм работает до тех пор, пока мы не добавим больше контента.
<body>
<div class="awesome-box">
<span>CSS часто преподносит сюрпризы, способные запутать даже опытных разработчиков. Я понимаю их раздражение. Тут всё закономерно.</span>
</div>
<div class="awesome-box">
<span>1/2</span>
</div>
</body>

Правда, есть несколько способов сохранить указанные значения. Один из них — это использование свойства overflow
со значением auto
, если его добавить к элементу, для которого задано свойство aspect-ratio
.
Давайте применим это решение к элементу .awesome-box
из первого примера.
.awesome-box {
width: 150px;
aspect-ratio: 1;
overflow: auto;
}

Внутри флекс-контейнера этот способ тоже работает. Только он возвращает механизм, в результате которого для всех флекс-элементов применится максимальное значение соотношения сторон.
body {
display: flex;
gap: 1rem;
}
.awesome-box {
width: 150px;
aspect-ratio: 1;
overflow: auto;
}
.awesome-box:nth-child(2) {
aspect-ratio: 1 / 2;
}

Второй способ вернуть указанное соотношение сторон — установить значение 0
для свойства min-height
. Заменю им свойство overflow
.
.awesome-box {
width: 150px;
aspect-ratio: 1;
min-height: 0;
}

То же самое сделаем в примере с флекс-элементами.
body {
display: flex;
gap: 1rem;
}
.awesome-box {
width: 150px;
aspect-ratio: 1;
}
.awesome-box:nth-child(2) {
aspect-ratio: 1 / 2;
}

Именование пользовательских CSS-свойств
В моей карьере был случай, когда я несколько часов тупил, не понимая, почему пользовательское CSS-свойство не работает. Для демонстрации я упростил код.
body {
--color: green;
background-color: var(--сolor);
}
Вы, наверное, думаете, что цвет фона у элемента <body>
должен быть зелёным? Я тоже так думал! Но нет, фон был белым.

Начав инспектировать код, я не обнаружил никаких ошибок.
Тем не менее, значение green
почему-то не применялось к пользовательскому свойству --color
. После долгих раздумий я плюнул и скопировал свойство --color
и вставил его в функцию var()
. И, о чудо, код заработал!
Оказывается, мы можем использовать в именовании пользовательских свойств как латинские, так и кириллические символы. Так вышло, что в строке --color: green
я использовал букву «c» на английском, а в строке background-color: var(--сolor)
— уже кириллическую «с». Для браузера это два совершенно разных пользовательских свойства, поэтому значение не применялось.
Признаться, я до сих пор не понимаю, почему кириллические символы в именах пользовательских свойств работают. Я пытался найти объяснение, но так и не нашёл ответа. Если вы знаете, пожалуйста, поделитесь информацией в комментариях. Мне было бы очень интересно узнать причину такого поведения.
Псевдо-класс :nth-child() с синтаксисом of S
На мой взгляд, синтаксис of S
для псевдо-классов :nth-child()
— одно из самых приятных нововведений в CSS за последние годы. Мне кажется, что каждому фронтенд-разработчику не хватало возможности более точного выбора элемента с определённым классом.
Однако, за моим первоначальным восторгом меня ждало разочарование. Перейду сразу к примеру.
<body>
<div class="awesome-box">1</div>
<div>2</div>
<div class="awesome-block">3</div>
<div class="awesome-box">4</div>
<div class="awesome-block">5</div>
</body>
:nth-child(2 of .awesome-box, .awesome-block) {
outline: 0.3rem dashed lightblue;
}

Я ожидал, что стили применятся ко второму элементу с классом .awesome-box
, а затем ко второму элементу с классом .awesome-block
, т.е. к двум последним элементам в примере. Но, как оказалось, это совсем не так.
Вся проблема заключается в понимании того, что именно представляет собой этот самый S
. Согласно стандарту, это всего лишь список селекторов (``). Как я понял, браузеры используют его в качестве перечня разрешённых селекторов. Сначала они находят все элементы, соответствующие этим селекторам, а затем уже среди них отбирают нужные.
Именно поэтому в моём случае браузеры сначала нашли все элементы с классами .awesome-box
и .awesome-block
, а затем применили стили к элементу, который оказался вторым в общем списке отобранных. Это был первый элемент с классом .awesome-block
.
Значение absolute внутри грид-контейнера
Когда я смотрю обучающий материал по вёрстке, то часто слышу фразу: «Координаты для свойств top
, right
, bottom
и left
у элемента с position: absolute
рассчитываются от элемента с нестатическим типом позиционирования».
Тут стоит пояснить, что такое нестатический тип позиционирования. Это любое значение для свойства position
, кроме static
. И да, это утверждение работает не всегда.
Я сразу перейду к примеру. Мы создадим два элемента. Родительский будет грид-контейнером с position: relative
и сеткой из пяти колонок и четырёх строк. У дочернего будет установлено свойство position
со значением absolute
и координатами, заданными свойствами top
и left
.
.awesome-block {
box-sizing: border-box;
min-height: 100dvh;
border: 3px solid currentColor;
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-template-rows: repeat(4, 1fr);
position: relative;
}
.awesome-block::before {
content: "";
width: 5rem;
height: 5rem;
background-color: darkolivegreen;
position: absolute;
top: 0;
left: 0;
}

Квадрат отображён в левом верхнем углу родительского элемента. Всё так, как нам говорили. А теперь смотрите магию.
Добавим свойства grid-column
и grid-row
для псевдо-элемента .awesome-block::before
.
.awesome-block {
box-sizing: border-box;
min-height: 100dvh;
border: 3px solid currentColor;
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-template-rows: repeat(4, 1fr);
position: relative;
}
.awesome-block::before {
content: "";
width: 5rem;
height: 5rem;
background-color: darkolivegreen;
grid-column: 2 / span 2;
grid-row: 2 / span 2;
position: absolute;
top: 0;
left: 0;
}

Квадрат поменял свою позицию.
Так получилось, потому что свойствами grid-column
и grid-row
мы задали координаты прямоугольника, в который помещается элемент. Его же используют браузеры для отсчёта позиции элементов со свойством position
и значением absolute
.
Поэтому квадрат отобразился в левом верхнем углу прямоугольника, созданного строками grid-column: 2 / span 2
и grid-row: 2 / span 2
, а не родительского элемента.
Мы можем даже сделать отступ от границ этой области. Например, добавим значение 1rem
для свойств top
и left
.
.awesome-block {
box-sizing: border-box;
min-height: 100dvh;
border: 3px solid currentColor;
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-template-rows: repeat(4, 1fr);
position: relative;
}
.awesome-block::before {
content: "";
width: 5rem;
height: 5rem;
background-color: darkolivegreen;
grid-column: 2 / span 2;
grid-row: 2 / span 2;
position: absolute;
top: 1rem;
left: 1rem;
}

Заключение
Подведём итог. В этой статье мы рассмотрели:
правила работы математической функции
calc
;в каких ситуациях заданное соотношение сторон будет сломано;
нюансы работы синтаксиса
of S
для псевдо-классовnth-child
иnth-of-type
;возможность задавать имена пользовательским CSS-свойствам на кириллице;
работу значения
absolute
внутри грид-контейнера.
Также мне интересно узнать, какие у вас были случаи, когда CSS запутывал. Расскажите, пожалуйста, о них в комментариях.
На этом всё. Спасибо за чтение!
P.S. Помогаю больше узнать про CSS в своём ТГ-канале CSS isn't magic. Присоединяйтесь. Ссылка в профиле.
© 2025 ООО «МТ ФИНАНС»
Комментарии (4)
nikolayshabalin
19.08.2025 09:18Признаться, я до сих пор не понимаю, почему кириллические символы в именах пользовательских свойств работают.
Звучит как байт на комментарий для меня, так как в статье есть ссылки на спецификации. Хитро.
Причина в том, что имена пользовательских свойств (
--var-name
) сравниваются браузером побайтно, а не «по смыслу» символов. То есть для движка--color
(с латинскойc
) и--сolor
(с кириллическойс
) — это два разных идентификатора.Это прямо указано в спецификации CSS Custom Properties for Cascading Variables Module Level 1:
Unlike other CSS properties, custom property names are not ASCII case-insensitive. Instead, custom property names are only equal to each other if they are identical to each other. [...] the "identical to" relation uses direct codepoint-by-codepoint comparison to determine if two strings are equal, to avoid the complexities and pitfalls of unicode normalization and locale-specific collation.
Поэтому браузер честно различает похожие на вид, но разные по коду символы (латинские/кириллические буквы, буквы с диакритикой, лигатуры и т.п.).
На практике это значит: если случайно использовать разные «похожие» буквы в имени переменной, мы получим два независимых свойства.
Чуть ниже, в спецификации, приводится пример как с виду похожие латинские символы это также разные идентификаторы, подтверждая что сравнение просходит по коду символов.:root { --fijord: red; --fijord: green; --fijord: blue; }
А ещё чуть ниже есть ссылка на механизм сопоставления строк, но я не уверен, что нужна такая глубина в этом вопросе.
Gromov32lvl
Статья полезная, многие такие “мелочи” реально выбешивают в работе. Особенно когда думаешь, что баг в коде, а это оказывается стандарт так работает.