Привет, Хабр!

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)


  1. Gromov32lvl
    19.08.2025 09:18

    Статья полезная, многие такие “мелочи” реально выбешивают в работе. Особенно когда думаешь, что баг в коде, а это оказывается стандарт так работает.


  1. 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;
    }


    А ещё чуть ниже есть ссылка на механизм сопоставления строк, но я не уверен, что нужна такая глубина в этом вопросе.


    1. melnik909 Автор
      19.08.2025 09:18

      спасибо!


      1. nikolayshabalin
        19.08.2025 09:18

        Вам спасибо за статью!