Условные выражения в CSS

Kate

Administrator
Команда форума
Мне нравится думать о CSS как о языке дизайна с условными выражениями. На протяжении многих лет CSS был известен как способ стилизации веб-страниц. Однако сегодня этот язык эволюционировал настолько, что в нём уже есть правила условных выражений. Любопытно то, что эти правила реализуются не напрямую (например, в CSS всё ещё нет if/else).

Инструменты дизайна наподобие Figma, Sketch и Adobe XD сильно облегчили жизнь дизайнеров, однако им всё равно не хватает той гибкости, которая есть у CSS.

В этой статье я расскажу о некоторых возможностях CSS, которые мы используем каждый день, и покажу, насколько они условны. Кроме того, я приведу несколько примеров, в которых CSS гораздо мощнее, чем инструменты дизайна.

Что такое условный CSS?​


Если говорить простыми словами, то имеется в виду дизайн с определёнными условиями. При удовлетворении одного или нескольких условий дизайн подвергается изменениям.

Например, вставка нового раздела в дизайн должна отталкивать вниз другие элементы под ним. На рисунке ниже в левой части показана стопка элементов. При добавлении нового элемента все остальные под ним должны сместиться вниз.

ajtxour1jzvikstf3mtyssxhepi.png


Логически это кажется ожидаемым и нормальным. В инструментах дизайна такая функция появилась много лет назад. В Figma есть функция «Auto Layout». В веб-дизайне это присутствовало изначально, даже до появления CSS.

Условный CSS​


Возможно, вы задаётесь вопросом: что такое условный CSS? Он вообще существует? Нет, в CSS отсутствует оператор «if».

Главное здесь — понять, что некоторые свойства CSS работают в определённых условиях или сценариях. Например, при использовании в CSS селектора :empty для проверки того, является ли элемент пустым, он работает в качестве условного псевдоселектора.

.alert p:empty {
display: none;
}

Если бы мне нужно было объяснять это своей двухлетней дочери, то я сделал бы это так:

Если здесь ничего нет, то это пропадёт.

Вы заметили здесь оператор «если»? Это дизайн с косвенно реализованными условными выражениями. В следующем разделе я объясню некоторые возможности CSS, работа которых похожа на работу оператора if/else.

В чём заключается цель этого? В более глубоком понимании и осознании того, что можно ждать от написанного тобой CSS. Вы научитесь находить условный CSS, просто взглянув на стили компонента, раздела или страницы.

CSS против Figma​


Почему Figma? Я считаю её современным стандартом для дизайна UX, поэтому подумал, что неплохо было бы выполнять сравнение на её основе. Покажу один простой пример. Вот список горизонтально отображаемых тегов.

pnrb62av3mobxfvbhjlfxetbnf8.png


Поразмыслив над ним, вы заметите важные различия. Например, версия на CSS:

  • может выполнять перенос на несколько строк, если недостаточно места;
  • работает с направлениями текста «слева направо» и «справа налево»;
  • при переносе элементов для строк использует gap.

Figma не имеет ничего из вышеперечисленного.

При этом в CSS присутствуют три условных правила:

  • Если flex-wrap имеет значение wrap, то элементы при нехватке места могут переноситься.
  • При переносе элементов на новую строку gap работает для горизонтального и вертикального пространств.
  • Если текст на странице идёт справа налево, то элементы переключат свой порядок (то есть Design будет первым справа).

И это всего лишь один пример, а я могу написать о них целую книгу. Давайте изучим несколько случаев, в которых CSS может быть условным.

Примеры условного CSS​


▍ Медиа-запрос​


Мы не можем говорить об условном CSS без упоминания медиа-запросов CSS. Спецификация CSS называется CSS Conditional Rules Module. Честно говоря, я впервые узнал об этом названии.

Когда я проводил своё исследование о тех, кто спрашивает о «Conditional CSS» или упоминает его, то часто видел, что ближайшим аналогом оператора if в CSS являются медиа-запросы (media query).

.section {
display: flex;
flex-direction: column;
}

@media (min-width: 700px) {
.section {
flex-direction: row;
}
}

Если ширина вьюпорта 700px или больше, изменить flex-direction элемента .section на row. Это ведь явный оператор if, правда?

То же самое справедливо и для медиа-запросов наподобие @media (hover: hover). В показанном ниже CSS стиль наведения будет применён, только если человек пользуется мышью или трекпадом.

@media (hover: hover) {
.card:hover {
/* Добавляем стили наведения. */
}
}

▍ Контейнерный запрос размера​


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

_vx_noofq7qt5lkar1xsh4vd4vg.png


.card-wrapper {
container-type: inline-size;
}

@container (min-width: 400px) {
.card {
display: flex;
align-items: center;
}
}

Я уже много раз писал о контейнерных запросах (container query) и создал ресурс, на котором делюсь связанными с ними демо.

▍ Контейнерный запрос стиля​


На момент написания этой статьи эта функция включалась флагом в Chrome Canary и её должны были выпустить в Chrome stable.

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

На рисунке ниже показано тело статьи, генерируемое из CMS. Мы видим стандартный стиль для изображения и другой стиль для изображения, которое отмечено как рекомендованное (Featured).

Чтобы реализовать это при помощи запросов стиля, мы можем стилизовать стандартный элемент, а затем проверить, имеет ли изображение специальную переменную CSS, позволяющую выполнить уникальную стилизацию.

figure {
container-name: figure;
--featured: true;
}

/* Стиль рекомендуемого изображения. */
@container figure style(--featured: true) {
img {
/* Уникальная стилизация */
}

figcaption {
/* Уникальная стилизация */
}
}

А если --featured: true отсутствует, мы по умолчанию будем использовать базовый дизайн изображения. Чтобы проверить, что изображение не имеет этой переменной CSS, можно использовать ключевое слово not.

/* Стандартный стиль изображений. */
@container figure not style(--featured: true) {
figcaption {
/* Уникальная стилизация */
}
}

Это оператор if, только косвенный.

Ещё один пример: изменение стилизации компонента на основании его родительского компонента. Рассмотрим следующий рисунок:

nk9kq8iu9bvw8gsmljt3vkd1cgo.png


Стиль карточки может переключаться на тёмный, если она помещена в контейнер, имеющий переменную CSS --theme: dark.

.special-wrapper {
--theme: dark;
container-name: stats;
}

@container stats style(--theme: dark) {
.stat {
/* Добавляем тёмные стили. */
}
}

Показанный выше пример означает следующее:

Если stats контейнера имеет переменную --theme: dark, то добавить следующий CSS.

supports в CSS​


Функция @supports позволяет тестировать, поддерживается ли в браузере конкретная функция CSS.

@supports (aspect-ratio: 1) {
.card-thumb {
aspect-ratio: 1;
}
}

Также мы можем выполнять тестирование на поддержку селектора, например :has.

@supports selector:)has(p)) {
.card-thumb {
aspect-ratio: 1;
}
}

▍ Перенос Flexbox​


Цитата из MDN:

Свойство CSS flex-wrap определяет, должны ли flex-элементы принудительно находиться в одной строке, или могут переноситься на несколько строк. Если перенос разрешён, оно задаёт направление, в котором строки накладываются друг над другом.

Свойство flex-wrap позволяет flex-элементам переноситься на новую строку, если недостаточно места.

Рассмотрим следующий пример. У нас есть карточка, содержащая заголовок и ссылку. Если места мало, каждый дочерний элемент должен переноситься на новую строку.

.card {
display: flex;
flex-wrap: wrap;
align-items: center;
}

.card__title {
margin-right: 12px;
}

otwshpomay5roz-lzz4q2cvmjvy.png


На мой взгляд, это условное выражение. Если места недостаточно, выполняем перенос на новую строку/строки.

ij3tcm1trp0rev724-so3qy5peq.png


Если каждый flex-элемент переносится на строку, то как управлять интервалами между flex-элементами? В настоящее время существует margin-right для заголовка, а в случае переноса элемента его следует заменить на margin-bottom. Проблема в том, что мы не знаем, когда элементы будут переноситься, потому что это зависит от содержимого.

Удобно то, что интервалы можно сделать условными при помощи свойства gap. Когда они находятся в одной строке, интервалы горизонтальные, а если в нескольких, то вертикальные.

.card {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1rem;
}

Это одна из самых любимых мной функций flexbox. Вот наглядная демонстрация того, как gap переключает интервалы.

gohhusemks1tgmilgp6cnn7pe3e.png


Кстати, я считаю flex-wrap защитным CSS. Чтобы избежать неожиданных проблем, я добавляю его почти в каждый flex-контейнер.

▍ Свойство flex​


Более того, свойство flex тоже может работать как условное выражение. Рассмотрим такой пример: я добавил к заголовку карточки flex-grow: 1, чтобы он заполнял доступное пространство.

.card__title {
flex-grow: 1;
}

rhiv9p0xe2wj0h71gcybd-8pf7g.png


Это работает, но когда ширина карточки слишком мала, её заголовок перенесётся на новую строку.

iwo2fcwui7ohr8-zbv4yrjual6y.png


Ничего особо ужасного, но можно ли сделать получше? Например, я хочу сказать заголовку: «Если твоя ширина меньше X, тогда перенесись на новую строку». Это можно сделать, задав свойство flex-basis.

В приведённом ниже CSS я присваиваю заголовку максимальную ширину 190px. Если она меньше, то он перенесётся на новую строку.

.card__title {
flex-grow: 1;
flex-basis: 190px;
}

thpf3vlg5j28ikkp0pievz27c74.png


Узнать больше о свойстве flex в CSS можно из моей подробной статьи о нём. В ней более глубоко рассматриваются такие вещи, как добавление flex-grow, string и так далее.

▍ Селектор :has​


На мой взгляд, сейчас это наиболее близкая к оператору «if» фича CSS. Она имитирует оператор if/else.

▍ Изменение стиля карточки​


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

grxosda3w30r89onoutvhyj1jbm.png


Если в карточке есть изображение:

.card:has(.card__image) {
display: flex;
align-items: center;
}

И если нет:

.card:not:)has(.card__image)) {
border-top: 3px solid #7c93e9;
}

Это же практически оператор if!

▍ Условное скрытие или отображение элементов форм​


В формах обычно бывает поле ввода или группа элементов ввода, скрытых по умолчанию, которые отображаются, когда пользователь активирует опцию в меню <select>.

slnt3ypkdhckxvthvdqgzhlepq8.png


При помощи :has можно проверять, выбрана ли опция other, и если она выбрана, отображать поле ввода.

.other-field {
display: none;
}

form:has(option[value="other"]:checked) .other-field {
display: block;
}

▍ Предупреждения​


Когда на странице есть предупреждающее сообщение, например, информация о серьёзной проблеме или проблемах в системе, его иногда нужно сделать ещё более очевидным.

_c2zdl8enzy5igwpgntv4gduwzi.png


В этом примере на странице есть предупреждение, а при помощи :has мы можем проверить, есть ли на дашборде предупреждение, и если есть, соответственно его стилизовать.

.main:has(.alert) .header {
border-top: 2px solid red;
background-color: #fff4f4;
}

Очень полезно.

▍ Изменение столбцов сетки на основании количества элементов​


Вам когда-нибудь было нужно отображать и менять ширину столбца в сетке на основании количества дочерних элементов?

fwwcyng2g2nkv94jrfbak_w_za8.png


С этим при помощи условных выражений позволяет справиться :has.

.wrapper {
--item-size: 200px;
display: grid;
grid-template-columns: repeat(
auto-fill,
minmax(var(--item-size), 1fr)
);
gap: 1rem;
}

.wrapper:has(.item:nth-last-child(n + 5)) {
--item-size: 120px;
}

В этом примере говорится, что если .wrapper содержит пять элементов, то переменная --item-size изменяет своё значение на 120px.

Подробнее о селекторе :has можно прочитать в моей статье, где есть множество примеров.

▍ Функция сетки minmax() в CSS​


Функция minmax() работает в CSS условным образом. Используя ключевое слово auto-fit, мы говорим браузеру: «если есть свободное пространство, то пусть элементы сетки заполняют его».

-qa1gfkxgnkla-9x_7jzmjtvrsg.png


▍ Комбинатор соседнего одноуровневого элемента​


Этот комбинатор сопоставляет второй элемент, идущий непосредственно после элемента.

В примере ниже если за элементом <h3> идёт элемент <p>, то <p> получает уникальные стили.

h3 + p {
margin-top: 8px;
}

yi9zjnqnnr5hmpsx6ete7ryzmpw.png


Верхнее поле <p> было изменено условным образом.

▍ Псевдокласс :focus-within​


Ещё одна интересная фича CSS — это :focus-within. Допустим, вам нужно проверить, находится ли элемент ввода в фокусе, и если да, добавить границу к его родительскому элементу.

Рассмотрим следующий пример:

gfjckh9gydmout088_eyi2bcgrq.png


У нас есть компонент поиска. Когда поле ввода в фокусе, вся обёртка должна иметь контур. При помощи :focus-within мы можем проверить, находится ли поле ввода в фокусе, и применить соответствующие стили.

.hero-form:focus-within {
box-shadow: 0 0 0 5px rgb(28 147 218 / 35%);
}

vholnc80pwdduexsscxwij4b9yg.png


▍ Селектор :not​


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

.item:not:)last-child) {
border-bottom: 1px solid lightgrey;
}

bgb5bkiqhem40al8ijr1qp7pz-u.png


▍ Условный border-radius​


Когда-то давно я писал о том, как нашёл интересный условный подход к добавлению border-radius для карточки на веб-сайте Facebook.

odj4cdqkszr4exf6muavvgk6d3s.jpeg


Идея в том, что когда карточка больше размера вьюпорта или равна ему по размерам, то радиус должен быть равен 8px, а если нет, то 0px.

.card {
border-radius: max(
0px,
min(8px, calc((100vw - 4px - 100%) * 9999))
);
}

Статью можно прочитать здесь.

▍ Условный разделитель​


Ещё один интересный пример использования условных выражений в CSS — создание разделителя, который переключает своё направление и размер на основании того, есть ли у элементов перенос.

Обратите внимание на разделитель между двумя разделами:

rufukh5sfich8refwkm2yz2rfha.png


Мне нужно, чтобы когда flex-элементы расположены один над другим, линия переключалась и становилась горизонтальной. Этого можно достичь, воспользовавшись flex-wrap и сравнением clamp.

.section {
--: 400px;
display: flex;
flex-wrap: wrap;
gap: 1rem;
}

.section:before {
content: "";
border: 2px solid lightgrey;
width: clamp(0px, (var(--breakpoint) - 100%) * 999, 100%);
}

Об этом написано в моём блоге, а решение с clamp() предложил Темани Афиф.

relnuwljf32ig9qluwvx10j-lem.png


▍ Внутреннее определение размеров: fit-content​


Ключевое слово fit-content — это сочетание min-content и max-content. Наверно, это не очень понятно, поэтому давайте взглянем на следующую блок-схему.

xjzrrhzws7wpgffxa-qi7tjfhgy.png


Если у нас есть элемент с width: fit-content, он работает условным образом в соответствии с блок-схемой.

h2 {
width: fit-content;
}

Вот видео о том, что происходит при изменении размеров:



Я писал о внутреннем определении размеров в своём блоге.

▍ Функции сравнения​


В CSS есть следующие функции сравнения: min(), max() и clamp(). Для меня они ощущаются условными в примере, на который я наткнулся в процессе написания недавней статьи.

lmqpjapnatflxtgrhjnfdfjprey.png


Идея в том, что у меня есть два разных контейнера: один для заголовка статьи (названия и даты), другой для основного содержимого и примечаний.

Я хочу выровнять край содержимого заголовка с содержимым тела.

Мне нужно, чтобы на мобильных отбивка слева была равна 1rem, однако в более крупных вьюпортах она должна быть динамической и зависеть от ширины вьюпорта.

mx0m4-ooo7aea4ipktjgqr1nvnq.png


Для этого можно использовать функцию max(), позволяющую условным образом выбирать одно из двух значений (1rem или динамическое значение).

.prose {
padding-left: max(1rem, (100vw - var(--wrapper-width)) / 2);
}

Подробнее об этой технике можно узнать из моей статьи Inside the mind of a frontend developer: Article layout.

▍ Псевдоклассы​


В CSS есть множество псевдоклассов, однако первым делом вспоминаются :focused и :checked.

input:checked + label {
/* Уникальная стилизация */
}

input:focus {
outline: 2px solid #222;
}

Если элемент ввода помечен, то добавляем эти стили к <label>. Если элемент ввода находится в фокусе… и так далее.

Но… CSS — это не язык программирования!​


Я знаю, спасибо, что сказали. Мне часто доводилось слышать этот аргумент. Лично у меня нет строгого мнения на этот счёт, но CSS во многих отношениях является условным.

На самом деле, большинство представленных выше примеров нельзя реализовать на JavaScript без условных операторов.

Заключение​


Мне понравилось писать эту статью, потому что она напомнила мне о причинах моей любви к CSS. Для меня CSS подобен суперсиле, потому что позволяет принимать множество дизайнерских решений при помощи его условных возможностей. Работа с инструментами дизайна часто кажется ограничивающей, потому что я как будто упираюсь в стены. Думаю, что возможность создания условных правил с помощью CSS делает его уникальным и мощным инструментом веб-дизайна.

Но это не значит, что я создаю дизайн в браузере. Инструменты дизайна для меня — это пустой холст для проб и экспериментов с дизайнерскими идеями, а также для создания отшлифованных UI-продуктов.


 
Сверху