[Перевод] Условные выражения в CSS

odj4cdqkszr4exf6muavvgk6d3s.jpeg


Мне нравится думать о 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;
  }
}


Если ширина вьюпорта 700 px или больше, изменить 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!

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


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