CSS-live.ru

Display: contents и его новые друзья спешат на помощь

Будни фронтенд-разработки то и дело подбрасывают нам задачки с взаимоисключающими условиями, а ограничения HTML и CSS делают их решение и вовсе невозможным (впрочем, за радость от преодоления этой «невозможности» многие из нас и любят эту работу).

Годами для многих, казалось бы, элементарных задач приходилось выбирать решение по принципу меньшего из зол. Сейчас стало полегче благодаря флексбоксам, вот-вот станет еще легче благодаря гридам. Но все системы раскладки, даже самые передовые, упираются в фундаментальное ограничение: CSS привязывается к DOM-элементам. А значит, нельзя собрать вместе и красиво вывести в одном контейнере дочерние элементы разных DOM-предков — как бы удобно, красиво, логично и адаптивно это ни было.

Так вот: теперь можно освободить элементы из-под этого «DOMашнего ареста» и работать с элементами разных уровней вложенности как с непосредственными соседями. Правда, пока в Firefox (добавлено 25.05.2017: уже не только, в Chrome 58+ примеры тоже работают, но нужно включить флаг «Экспериментальные функции веб-платформы» в chrome://flags, добавлено 06.12.2017: а теперь еще и в Safari TP 45, добавлено 07.03.2018: и наконец в стабильном Chrome 65+ без флага!). Так что желательно открыть один из этих браузеров, чтобы увидеть примеры в действии.

Display:contents. Что это и откуда

Мы привыкли к простым значениям display: inline, block, inline-block. Теперь вот flex. Изредка еще table(-cell). Постоянные читатели нашего сайта вспомнят периодическую таблицу display, где их побольше — но даже там не было никакого contents. Откуда он вообще взялся?

Это значение описано в новой спецификации CSS Display module level 3. На момент выхода статьи по ссылке выше он был первым очень сырым черновиком, теперь же это солидная спецификация с минимумом красных пометок. Так что самое время взяться за нее — там вообще много нового и необычного. А про наше значение contents там сказано так:

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

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

Display:contents. Примеры использования

Меню с логотипом в одном флекс-контейнере

Возможно, вы уже видели пример Джейка Арчибальда, в котором два потомка флекс-элемента благодаря display:contents для их родителя превращаются в самостоятельные флекс-элементы. Вот похожий пример, более приближенный к жизни:

See the Pen PZaxoK by Ilya Streltsyn (@SelenIT) on CodePen.

Здесь причуда дизайнера заставила нас вставить логотип между пунктами меню, и вся эта конструкция должна равномерно растягиваться с равными промежутками. Без display:contents нам пришлось бы либо запихивать логотип внутрь меню (прощай, семантика…), либо делить само меню на две части и «колдовать» с отступами. А с ним мы делаем пункты меню и логотип равноправными флекс-элементами, а дальше магия флексбоксов — включая изменение визуального порядка элементов — делает всё за нас сама!

Дерево-таблица

Часто бывает нужно представить в табличном виде иерархические данные, например, значения какого-то интегрального показателя и его разбивку по составляющим. Пример из нашей сферы — результаты разных браузеров в популярном тесте html5test.com: суммарные баллы по разделам спецификации и их «разбивка» по подразделам.

Данные явно табличные, но вот беда — табличная модель HTML практически полностью исключает иерархичность. Максимум, что мы можем — выделить главные разделы таблицы в отдельные tbody. Дальше дробить таблицу нельзя. Поэтому на странице самого теста пришлось плодить отдельные классы для строк таблицы каждого уровня вложенности, нетривиальную логику для их скрытия-раскрытия и т.п. Надо ли говорить, что без стилей и скриптов вся информация об иерархии теряется.

Иерархические структуры в HTML можно передавать с помощью вложенных списков. Что, если так и сделать, а табличное отображение придать с помощью CSS? Увы, табличная модель CSS унаследовала от HTML-таблиц не только структуру, но и ограничения: в боксе с display:table могут «жить» только table-*-group, а в тех — table-row. Было бы здорово, если бы table-row-group можно было вкладывать, но увы — алгоритм не разрешает. Вместо пропущенных элементов табличной иерархии достраиваются недостающие анонимные боксы, и получается обычная скучная вложенная таблица, лишенная главного табличного плюса — сквозной вертикальной связи по столбцам.

Но с display:contents эта задача решается в два счета (для примера приведено только начало таблицы):

See the Pen Demo of hierarchical data table using display:contents by Ilya Streltsyn (@SelenIT) on CodePen.

Мы просто сделали вид, что промежуточных уровней иерархии… нет вообще! И все строки таблицы (элементы p) подчинены главной таблице (обертке с class="table") напрямую. И все вертикальные связи сохранились!

Но вложенность никуда не исчезла — мы ведь не трогали DOM, мы лишь изменили display некоторых элементов. Поэтому мы по-прежнему можем, например, визуально разграничить элементы разных уровней отступами, основываясь на этой вложенности (в примере как раз это показано). Более того, теперь скрывать/раскрывать подуровни стало элементарно — как с обычным древовидным списком. Только обычно мы меняли display с none на block и обратно, а здесь — c none на contents и обратно. Не нужно больше циклами выискивать строки для скрытия по классам и т.п.! Удобно же, правда?

Решение проблемы подсеток в грид-раскладке

Отвязка визуальной структуры от DOM-структуры позволяет решить и ту проблему, на которую недавно сетовал Эрик Мейер в статье о грид-раскладке (мы как раз ее переводили): невозможность привязать элементы к гриду, создаваемому родителем их родителей. С display:contents это элементарно: как и в предыдущих примерах, с точки зрения визуальной структуры мы делаем так, что «дети» элемента не отображаются вообще, а его «внуки» отображаются так, как будто они «дети» — благодаря чему они автоматом подхватывают «магию» грид-раскладки, созданную внешним контейнером. В Firefox (либо Chrome c включенным флагом «Экспериментальные функции веб-платформы») можно увидеть это в действии.

Обратите внимание, что кода получается даже меньше, чем в решении Эрика с подсетками. Хак ли это? На мой взгляд — не больший, чем любая другая смена display на «неродное» значение. У многих задач бывает несколько правильных решений, и это — вполне стандартное применение стандартных средств языка. И уж точно не стоит откладывать выпуск практически готовой реализации грид-раскладки ради непременной реализации подсеток, когда есть такой изящный и наглядный обходной путь. В споре Эрика и Таба Аткинса лично я поддерживаю Таба. А вы что думаете?

Поддержка (и можно ли ее улучшить)

Надеюсь, сомнений в полезности новинки у вас не осталось, но поддержка браузерами пока всё портит. Единственное место, где для display:contents просматривается какая-никакая замена — это инлайновый контекст форматирования, в котором контейнеру можно поставить display:inline, что поместит его содержимое (напр. инлайн-блоки) в одну строку с содержимым его родителя. Это может подойти для редких и простых частных случаев вроде такого, но для самых «вкусных» применений — вроде примеров из статьи — к сожалению, такой замены не видно.

Остается лишь «теребить» разработчиков браузеров, чтобы они поскорее внедрили поддержку такого полезного значения (к тому же вряд ли сверхсложного в реализации). Проще всего, наверное, будет добиться этого от MS Edge. Пожалуйста, проголосуйте за поддержку этого свойства там!

Бонус: другие полезные новинки CSS Display level 3

Нужно сразу оговориться, что полезны они лишь в теории: в отличие от display:contents, они пока не поддерживаются нигде. Но такие многообещающие!

Первым я бы упомянул свойство box-supress. Помните, сколько мучений доставляла нам отмена display:none, особенно на этапе ученичества? Стоило лишь впопыхах поставить вместо него display:block строке таблицы или текстовому элементу, как вся верстка сыпалась как карточный домик. Это новое свойство обещает избавить от этих мучений навсегда. Оно позволит убирать элемент из верстки и возвращать обратно, не «забывая» о том, как он отображался прежде. На данный момент спецификация предлагает для него 3 значения: show — отображать как обычно, discard — скрыть напрочь (как при display:none), и hide — нечто совсем интересное: «отобразить, но не показывать» (построить визуальную структуру как обычно, со всеми размерами и т.п. — но не выводить ее на экран!). Наверное, это последнее значение пригодится для динамического скрытия/показа элемента с анимацией.

Добавлено 22.02.2017. Cвойство box-supress решили отложить на следующий этап, на CSS Display Level 4. В нынешней спецификации его больше нет.

Не менее «революционное» изменение затронет и обычные значения свойства display, особенно те, которые сами «живут» в одном контексте форматирования, а внутри себя создают другой. Теперь и то и другое можно будет указывать явно и по отдельности — значение display:none разобьют на 2 отдельных ключевых слова, каждое из которых отвечает за «внешнюю» и «внутреннюю» стороны отображения элемента соответственно. А наши старые привычные «значения с дефисами» станут лишь псевдонимами для некоторых таких комбинаций. И появится еще одно полезное значение display:block flow-root: оно наконец позволит решить проблему замены «clearfix-у» полностью, без издержек любого из существующих решений (добавлено 9.05.2017: уже появилось!). Другой вопрос — будет ли это по-прежнему актуально, в эпоху повсеместной грид-раскладки:)

Так что закончим статью стандартным уже призывом читать спецификации:) И, конечно, делиться своими соображениями и находками в комментариях!

P.S. Это тоже может быть интересно:

9 комментариев

  1. Я не понимаю для чего этот избыток скрывающих/показывающих css свойств: display, visibility, box-supress, и к ним в догонку еще html-атрибут hidden.
    Мне кажется это только приведет к путанице.
    Чем отличается box-supress: hide от visibility: hidden?
    По-моему естественней было бы доработать поведение display: initial для того, чтобы учитывался тип элемента, а не ставился inline по-умолчанию, а не вводить еще одно свойство.

  2. Чем отличается box-supress: hide от visibility: hidden?

    Насколько я сам понял, при box-supress: hide элемент рисуется «как бы в уме» — некий аналог documentFragment-а, только не для DOM, а для дерева рендеринга. Он получается не связан с другими выводимыми элементами, но может использоваться для измерения чего-нибудь, например (сейчас иногда для этой цели используют абсолютно позиционированные элементы с visibility: hidden, но это куда больше смахивает на хак). Возможно, я и заблуждаюсь.

    доработать поведение display: initial для того, чтобы учитывался тип элемента, а не ставился inline по-умолчанию

    К сожалению, это не возможно по определению initial. Возможно, такое получится с revert из нового черновика.

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

  4. Правильно ли я понимаю, что во втором примере с таблицей, мы просто для (назначенной) таблицы скрыли иерархию? А иерархие нужна в html, или, вернее, может понадобится для других целей? (например, каких?)
    И второе — немного не в тему, но интересно — почему ссылка на скрин для не поддерживаемых бразузеров, такая ахово-большая?

    1. Иерархия осталась в DOM и вполне может пригодиться и в CSS (в примере это тоже показано — разное оформление разных уровней, скрытие/показ подуровней). Но да, сами промежуточные элементы у нас только «в уме», визуально они никак себя не проявляют — как при display:none. Главное отличие тут в том, что если none распространяется и на потомков, у элемента с display:none и все потомки исчезают, то при display:contents потомки этого элемента остаются видимыми и добавлются в дерево отрисовки вместо «пропавшего» родителя.

      А скрины для неподдерживаемых просто заинлайнены прямо в код, в base64 — просто чтоб не заливать их отдельно на хостинг:)

      1. Получается блок с display:contents ставит для своих потомков как бы (математические? или логические) скобки — они объединены и находятся на одном уровне с остальными. С точки зрения же оформления, это похоже на класс.

        1. Имхо, не столько «объединяет» в скобки, сколько, наоборот, «раскрывает» их — убирает обертку и подставляет содержимое напрямую, в «голом» виде.

  5. Вы пишите о симантичности а сами в ссылку поместили h1 и папарграф…

    Или это просто для примера…?

    1. Спецификация HTML такое разрешает, один из примеров в ней показывает вообще целую секцию с заголовком, обернутую в ссылку. Семантически это как раз верно, если заголовок — часть единой кликабельной области, логично ему быть внутри ссылки. Другое дело, что скринридеры и т.п. пока плохо умеют передавать такую семантику, поэтому ради доступности действительно лучше не класть больших блоков контента в ссылку — но это проблема не семантики как таковой, а ограничений конкретных технологий доступности (надеюсь, временных). И на момент выхода статьи эта проблема была не настолько известна и актуальна:(

Оставить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

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