[Перевод] CSS-селектор :has() и междустрочные интервалы в длинных текстах

c089b4e652c15967550b753561c5f518.png

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

Писать такие стили удивительно непросто. Именно поэтому появились инструменты, подобные плагину Tailwind Typography и Prose от Stack Overflow, хотя они работают далеко не только с междустрочными интервалами.

На момент написания этой статьи Firefox поддерживает :has() при включении флага layout.css.has-selector.enabled в about:config.


Почему с междустрочными интервалами между элементами типографики сложно работать?

Казалось бы, достаточно указать, что у каждого элемента — p, h2, ul и т.д. — должна быть определённая величина верхнего и/или нижнего внешнего отступа (margin), так? К сожалению, не всё так просто. Предположим, что нужно выполнить такие требования:


  • Сверху первого и снизу последнего элемента в блоке с длинным текстом не должно быть никакого дополнительного пространства, чтобы нетипографские элементы вокруг текста располагались правильно.
  • Между секциями в длинном тексте должен быть большой интервал. «Секция» здесь — это заголовок и весь текст, который к нему относится: большой интервал нужен перед заголовком, но не тогда, когда сразу перед заголовком находится ещё один!

c089b4e652c15967550b753561c5f518.png
После h3 находится параграф, а после h2 — ещё один h3.

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

Посмотрим, где это пригодится. Пара скриншотов с интервалами из другой статьи:

2b7b8629e80cc755ca06c26c15e9cc2e.png
h2 прямо над h3.

21a6399a1f223ecd4e4265b7468015d3.png
h3 сразу после параграфа и междустрочный интервал.


Традиционное решение

Как правило, эту задачу решают, оборачивая длинное содержимое в div (или в семантический тег при необходимости). Обычно я называю обёртку .rich-text — это наследие старых версий Wagtail CMS, которые добавляли этот класс автоматически при рендеринге WYSIWYG-контента. Tailwind Typography использует класс .prose (и ещё кое-какие классы-модификаторы).

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

Традиционное решение выглядит логично…, но в чём проблема? Мне кажется, что проблем здесь несколько.


Жёсткая структура

Необходимость добавлять класс-обёртку вроде .rich-text означает внедрение особой структуры в HTML-код. В данном случае этого не требуется. Легко забыть добавить такой класс в нужное место, особенно при использовании смеси CMS и контента, в котором изменения не предусмотрены.

Структура HTML теряет гибкость, если следует убрать верхний и нижний внешние отступы у первого и последнего элементов, так как они должны быть прямыми потомками элемента-обёртки, к примеру, важен селектор .rich-text > *:first-child. >, ведь с его помощью мы случайно выбраем первый элемент в каждом ul или ol.


Использование разных сторон внешних отступов

До появления :has() не было способа выбрать элемент в зависимости от следующего элемента. А значит, традиционный подход к созданию междустрочных интервалов типографских элементов — использовать и margin-top, и margin-bottom:


  1. Сначала с margin-bottom определяем размер междустрочного интервала по умолчанию.
  2. Затем, при помощи смежного селектора (к примеру, h2 + h3), создаём междустрочный интервал для «секций» через margin-top — например, большой интервал перед каждым заголовком.
  3. Перезаписываем эти большие интервалы в случае, когда за заголовком сразу следует ещё один.

Не знаю, как вам, а мне всегда казалось, что при определении междустрочных интервалов лучше использовать только одну сторону отступа, как правило, margin-bottom (предполагая, что в данном случае CSS-свойство gap неприменимо. Стоит ли так работать, я оставляю на ваше усмотрение. Но для установки междустрочных интервалов длинного контента я предпочитаю margin-bottom.


Схлопывание внешних отступов

Из-за схлопывания внешних отступов одновременное применение margin-bottom и margin-top само по себе не проблема. Из двух расположенных друг над другом внешних отступов виден будет только больший, а не сумма значений отступов. Но мне не нравятся схлопывающиеся внешние отступы. Их также стоит принять во внимание.

Схлопывание отступов может запутать начинающих разработчиков, не знающих об этой особенности CSS. Междустрочные интервалы изменятся (к примеру, прекратят схлопываться), если придать обёртке, скажем, свойство flex с flex-direction: column. Этого не произойдёт, если использовать только одну сторону отступа при задании вертикальных внешних отступов.

Я более-менее понимаю, как работает схлопывание внешних отступов, и осознаю, что так сделано специально. Иногда оно помогает, но иногда — нет. Мне кажется, что схлопывание — странная штука, и, как правило, я стараюсь избегать его.


Решение через :has()

Напомню о том, чего я хочу добиться:


  • Избавиться от класса-обёртки.
  • Использовать только одну сторону внешнего отступа.
  • Избежать схлопывания внешних отступов (вы можете считать это улучшением или нет).
  • Избавиться от установки стилей и их немедленного перезаписывания.

Вот как я попытался решить эти проблемы через :has().


Заметки и пояснения для решения с помощью :has()


  • Всегда проверяйте, поддерживает ли браузер :has(). На момент написания этой статьи Firefox поддерживает :has() только после установки экспериментального флага.
  • Моё решение не учитывает все существующие элементы типографики. К примеру, в моей демоверсии нет поддержки
    . Но список селекторов легко расширить.
  • Моё решение не работает с нетипографскими элементами, которые могут присутствовать в определённых блоках с длинным текстом, к примеру, . Дело в том, что я работаю с сайтами, где WYSIWYG-редакторы максимально ограничены основными текстовыми элементами, такими как заголовки, параграфы и списки. Другие элементы (цитаты, картинки, таблицы и т.д.) находятся в отдельном компонентном блоке CMS. Эти блоки при рендеринге на странице разделены интервалами. Но, повторю, список селекторов легко дополняется.
  • Я включил h1 только для полноты картины. Обычно я не разрешаю использование h1 в WYSIWYG-редакторе: так заголовок страницы оказывается где-то в макете, а изменять его нужно с помощью CMS-редактора страницы.
  • Я не предусмотрел ситуацию, когда за одним заголовком сразу следует другой того же уровня (h2 + h2). Это означало бы, что у первого заголовка нет контента, что похоже на неправильное применение заголовков (и поправьте меня, если я ошибаюсь, но это может нарушать принципы руководства по доступности веб-содержимого WCAG 1.3.1 Info and Relationships)). Я также не предусмотрел пропуска уровней заголовков.
  • Я никоим образом не умаляю достоинства ранее упомянутых мною подходов. Когда я создаю сайт с помощью Tailwind, то, конечно же, использую великолепный плагин Typography.
  • Я не дизайнер. Я задал междустрочные интервалы на глаз. Вам же следует использовать более подходящие значения.


Специфичность и структура проекта

Здесь я собирался написать большой раздел о соответствии традиционного подхода и нового решения с помощью :has() [методологии ITCSS… Но теперь есть :where() (селектор с нулевой специфичностью), так что можно выбирать подходящее значение специфичности для любого селектора.

Однако обёртка вроде .prose, .rich-text и т. д. больше не используется. Это заставляет меня думать, что они необходимы на уровне «элементов», то есть перед тем, как вы займётесь специфичностью на уровне классов. В своих примерах для поддержания специфичности я использовал :where(). Все селекторы в моих примерах обладают значениями специфичности, равными 0,0,1 (за исключением простого сброса).


Заключение

У вас на руках ультрасовременное решение весьма занудной задачи! Этот новый подход я бы не назвал «простым». Как я уже говорил в начале статьи, но эта задача сложнее, чем может показаться на первый взгляд. Но, помимо наличия нескольких несложных селекторов, мне кажется, что такой подход куда логичнее, а менее жёсткая структура HTML весьма привлекательна.

Если вы будете использовать моё решение или что-то на него похожее, расскажите мне о результатах. А если вы найдёте способ сделать его лучше, поделитесь со мной своими открытиями!

rz4hnexx9lidivxbzuaheff5usq.png


Краткий каталог курсов

Data Science и Machine Learning

Python, веб-разработка

Мобильная разработка

Java и C#

От основ — в глубину

А также


© Habrahabr.ru