Привет, Хабр. Я продолжаю делиться «косяками» CSS, которые могут сбить нас с толку. Мне не нравится, что многие плюются от него. Конечно, не без оснований. Я их понимаю, потому что тоже плевался.


Только мне нравится CSS. Хоть и потратил множество часов на изучение неочевидных моментов, я не хочу, чтобы у языка была слава «костыля». Подумав, как помочь другим меньше мучиться, я собираю и объясняю неочевидности CSS.


Сегодня мы рассмотрим:

  • Нюансы работы синтаксиса of S для псевдо-класса :nth-child();
  • Случаи использования свойства aspect-ratio неожиданно ломающие вёрстку;
  • Различия между псевдо-классом lang() и селектором по атрибуту [lang=""].

Давайте посмотрим, что я вам подготовил.


▍ Подвох в синтаксисе of S для псевдо-класса :nth-child()


Синтаксис of S для псевдо-классов :nth-child() одна из моих любимейших нововведений в CSS за последние года. Мне кажется, что каждому фронтендеру не хватало возможности отобрать конкретный элемент определённого класса.


За моим восторгом меня ждало разочарование. Давайте на примере покажу вам.


<body>
  <div class="primary">1</div>
  <div>2</div>
  <div class="secondary">3</div>
  <div class="primary">4</div>
  <div class="secondary">5</div>
</body>

:nth-child(2 of .primary, .secondary) {
  outline: 0.3rem dashed coral;
}

У меня к вам вопрос: «К каким элементам применятся стили?». Я думал, что сначала ко второму элементу с классом .primary, а потом ещё ко второму элементу с классом .secondary. Оказывается, это не так.


5 квадратов отображены в линию. У третьего есть обводка

Нюанс, сбивший меня с толку, заключается в том, что такое S. В стандарте сказано, что этот символ является списком селекторов (<complex-selector-list>). Как я понимаю, браузеры используют его, как список «разрешённых» селекторов. С помощью него они находят все подходящие элементы, а потом среди них отбирают нужные.


По этой причине в моём случае браузеры сначала нашли все элементы с классами .primary и .secondary, а потом среди них применили стили к элементу, являющимся вторым отобранным, и первым с классом .secondary.


Конечно, от этой особенности я не поменял отношение к синтаксису. Я часто его использую. Главное теперь помнить, как же он работает.


▍ В каких случаях свойство aspect-ratio ломает вёрстку


Свойство aspect-ratio было одним из неизвестных мне. Конечно, я знал, что оно делает. Но без деталей. Казалось, что сложностей при работе с ним нет. Давайте посмотрим, так ли это.


В стандарте CSS Box Sizing Module Level 4 сказано, что свойство устанавливает предпочтительное соотношение сторон (preferred aspect ratio). Оно будет использоваться для вычисления автоматических значений размеров и в других задачах при компоновке страницы.


В качестве примера создам квадратный элемент .awesome-box.


<body>
  <div class="awesome-box"></div>
</body>

.awesome-box {
  width: 150px;
  aspect-ratio: 1;
}

Отображается квадрат

Вроде всё действительно просто. На всякий случай добавлю текст для элемента.


<body>
  <div class="awesome-box">
    <p>
       The aspect-ratio CSS property allows you to define the desired width-to-height ratio of an element's box. This means that even if the parent container or viewport size changes, the browser will adjust the element's dimensions to maintain the specified width-to-height ratio.
    </p>
  </div>
</body>

Текст увеличил высоту квадрата, поэтому он перестал быть им

Тут уже наш квадрат перестал быть таковым. Может показаться, что это ошибка. Это не так. Все значения свойства учитывают разные условия, влияющие на размеры элемента. Контент —
одно из них. Если он должен занимать больше места, то это предпочтительное соотношение сторон сломается.


Другим примером являются элементы, находящиеся внутри флекс-контейнера.


<body>
  <div class="container">
    <div class="awesome-box">1/1</div>
    <div class="awesome-box">1/2</div>
  </div>
</body>

.container {
  display: flex;
  gap: 1rem;
}

.awesome-box {
  width: 150px;
  aspect-ratio: 1;
}

.awesome-box:nth-child(2) {
  aspect-ratio: 1 / 2;
}

Два флекс-элемента. Они одинаковой высоты

По умолчанию флекс-элементы растягиваются по дополнительной оси. Если к ним ещё применяется свойство aspect-ratio, оно сработает по-особому. У всех элементов будет установлено то значение, в результате которого по дополнительной оси будет максимальное значение.


В нашем примере размер по дополнительной оси у первого элемента 150px, поскольку у него установлено значение 1 для свойства aspect-ratio. Для второго элемента я использую значение 1/2, поэтому значение по дополнительной оси будет 300px. Оно больше первого, поэтому браузеры применяют значение 1/2 для всех флекс-элементов.


Если мы добавим контент, то он также может нарушить предпочтительное соотношение сторон.


Два флек-элемента. Внутри первого отображен текст. Высота у элементов одинаковая

В стандарте описаны несколько случаев, заставляющих сохранить указанное значение. В первом используется свойство overflow со значением auto. Его нужно добавить к элементу, у которого задано свойство aspect-ratio.


Сделаем это для элемента .awesome-box из первого примера.


.awesome-box {
  width: 150px;
  aspect-ratio: 1;
  overflow: auto;
}

Есть элемент в виде квадрата с полосой прокрутки. Внутри него текст

Внутри флекс-контейнера мы тоже можем использовать этот способ. Только будет одна особенность. Браузеры вернут максимальное соотношение сторон для всех флекс-элементов.


.container {
  display: flex;
  gap: 1rem;
}

.awesome-box {
  width: 150px;
  aspect-ratio: 1;
  overflow: auto;
}

.awesome-box:nth-child(2) {
  aspect-ratio: 1 / 2;
}

Два флекс-элемента. Высота обоих рассчитана по второму элементу. Внутри первого находится текст. Также он с полосой прокрутки

Вторым случаем является свойство min-height. Если мы объявим его со значением 0, то свойство aspect-ratio заработает. Давайте заменим свойство overflow из первого примера.


.awesome-box {
  width: 150px;
  aspect-ratio: 1;
  min-height: 0;
}

Отображен элемент в виде квадрата. Текст внутри него выходит за его пределы

В примере с флекс-контейнером свойство также заставляет работать свойство aspect-ratio.


.container {
  display: flex;
  gap: 1rem;
}

.awesome-box {
  width: 150px;
  aspect-ratio: 1;
  min-height: 0;
}

.awesome-box:nth-child(2) {
  aspect-ratio: 1 / 2;
}

Два флекс-элемента. Высота первого повторяет высоту второго. У первого есть текст внутри. Он выходит за пределы элемента

Забавно ещё то, что если мы изменим направление осей, то результат будет тем же. Для примера я добавил свойство flex-direction со значением column.


.container {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.awesome-box {
  width: 150px;
  aspect-ratio: 1;
  min-height: 0;
}

.awesome-box:nth-child(2) {
  aspect-ratio: 1 / 2;
}

Два флекс-элемента отображены друг под другом. Первый элемент отображен в виде квадрата. Второй в виде прямоугольника. Текст из первого флекс-элемента выходит за его пределы и прячется под вторым флекс-элементом

Все эти моменты сказались на моё отношение к свойству aspect-ratio. Я чаще избегаю его, чем использую. Исключением является изображения и видео. Об этом я рассказал в другой статье.


▍ Разница между псевдо-классом :lang() и селектором по атрибуту [lang=""]


В августе я опубликовал статью «Неизвестно полезный CSS. Часть 4». В ней поделился псевдо-классом :lang(). В комментариях были люди, которые подумали, что его можно заменить селектором по атрибуту [lang=""]. В общем, всё получилось, как у меня при первом знакомстве с псевдо-классом. Конечно, тогда я ошибался.


В стандарте «Selectors Level 4» дано определение. Псевдо-класс :lang представляет элемент, язык контента которого является одним из языков, перечисленных в его аргументе. Другими словами, браузеры найдут абсолютно все элементы, у которого установлен язык, указанный в качестве аргумента. Начнут с элемента, у которого установлен атрибут lang, а закончат последним дочерним.


Браузеры применят свойство background-color абсолютно ко всем элементам в следующем примере.


<body>
  <section lang="en">
    <h2>The :lang() pseudo-class</h2>
    <div>
      <p>Псевдо-класс CSS :lang() выбирает элементы, основываясь на языке, на котором они определены</p>
    </div>
  </section>
</body>

Стили применяются к параграфу с текстом

Если мы установим атрибут lang с другим значением для дочернего элемента, то с него браузеры перестанут применять стили. Для примера я установлю lang="ru" для элемента <div>.


<body>
  <section lang="en">
    <h2>The :lang() pseudo-class</h2>
    <div lang="ru">
      <p>Псевдо-класс CSS :lang() выбирает элементы, основываясь на языке, на котором они определены</p>
    </div>
  </section>
</body>

Стили не применяются к параграфу с текстом

А вот с селектором по атрибуту такого поведения не будет. Стили применятся только к элементу, у которого установлен атрибут lang.


Есть ещё одно отличие. Правда, маловероятно, что вы его встретите на практике. Но мне было любопытно узнать о нём.


В стандарте есть ссылка на документ RFC4647. Как я понял, он описывает правила, по которым браузеры понимают язык контента. Оказывается, объявленное значение для атрибута lang является тегом языка.


Мы не будем подробно их рассматривать. Для статьи нам важно узнать, что теги состоят из подтегов. Они помогают определить различные диалекты и их правила.


Разница между псевдо-классом lang() и селектором по атрибуту заключается в том, что первый найдёт элементы на указанном языке, включая все диалекты. По этой причине в следующем примере свойство background-color применяется для двух элементов, а свойство font-weight только для второго.


<body>
  <p lang="de-CH">Dies ist ein Wert</p>
  <p lang="de">Dies ist ein Wert</p>
</body>

:lang(de) {
  background-color: pink;
}

[lang="de"] {
  font-weight: bold;
}

Два элемента с розовым фоном. У второго, текст выделен жирным

Можно заморочиться и заставить селектор по атрибуту найти оба элемента, но если вы прочитаете правила формирования тегов языка из стандарта RFC4647, то поймёте, что это плохая идея.


Подводя итоги раздела, мне хочется сказать, что псевдо-класс :lang() сильно недооценён. Мне он показался бесполезным, поэтому я не знал о нюансах. Так что этот случай стал для меня хорошим напоминанием, что нужно быть любознательным.


▍ Заключение


Давайте подведём итог. В этой статье мы рассмотрели следующее:

  • Браузеры при помощи синтаксиса of S сначала находят все элементы, а только потом среди них находят нужный;
  • По умолчанию значения свойства aspect-ratio можно нарушить с помощью контента, но свойства overflow и min-height отменяют это поведение;
  • Внутри флекс-контейнера заданное с помощью свойства aspect-ratioсоотношение сторон рассчитывается по максмальному значению по дополнительной среди всех флекс-элементов;
  • Псевдо-класс lang() срабатывает для элемента, у которого задан атрибут lang, а также для всех его дочерних элементов, пока среди них не встретится другой элемент с атрибутом lang;
  • Также этот псевдо-класс срабатывает для всех диалектов.

Оставляю ссылки на все выпуски:

Также, пожалуйста, напишите в комментариях, какие у вас были случаи, когда CSS сбил вас с толку. Буду ждать их. Спасибо за чтение!


P.S. Помогаю больше узнать про CSS в своём ТГ-канале CSS isn't magic. Присоединяйтесь. Ссылка в профиле


Telegram-канал со скидками, розыгрышами призов и новостями IT ?

Комментарии (3)


  1. Qoragar
    24.09.2024 11:55

    Также, пожалуйста, напишите в комментариях, какие у вас были случаи, когда CSS сбил вас с толку.

    Drop-shadow + анимированный clip-path. Внезапно столкнулся несколько лет назад.

    Казалось бы, всё норм, тенька отлично ложится на любой элемент с clip-path . Но как только clip-path анимируешь... Упс, drop-shadow говорит "я устала, я ухожу" — после начала анимации просто исчезает, и снова появляется на элементе только в финальном состоянии анимации. Наблюдалось во всех браузерах.

    Методом (не)научного тыка удалось выяснить, что тенька работает на всём протяжении анимации, только если обернуть элемент с clip-path в дополнительный внешний контейнер-пустышку, назначив drop-shadow уже на него. Зачем, почему — сие науке так до сих пор и неизвестно...

    Особую пикантность ситуации придаёт тот факт, что аккуратно и чётко работать по границам clip-path умеет только drоp-shadow — никакие другие виды теней не справляются.


    1. director-rentv
      24.09.2024 11:55

      Подобный нюанс видал как минимум в Firefox с backdrop-filter: blur. Если установить его элементу, а затем анимировать его передвижение через Web Animation API, то на время анимации блюр исчезает (всё чёткое), а в моменты остановки - снова появляется.


      1. Qoragar
        24.09.2024 11:55

        А попробуйте на внешнем контейнере-"обёртке", как я выше описал. Интересно, это какой-то общий косяк фильтров, или только определённых?