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

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

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

Так вот: теперь освободить элементы из-под этого «DOMашнего ареста» можно. Правда, пока только в одном браузере — Firefox. Так что желательно открыть его, чтобы увидеть примеры в действии.

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 45 и выше (либо если включить настройку layout.css.grid.enabled в about:config в Firefox 43–44) можно увидеть это в действии.

Обратите внимание, что кода получается даже меньше, чем в решении Эрика с подсетками. Хак ли это? На мой взгляд — не больший, чем любая другая смена 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 — нечто совсем интересное: «отобразить, но не показывать» (построить визуальную структуру как обычно, со всеми размерами и т.п. — но не выводить ее на экран!). Наверное, это последнее значение пригодится для динамического скрытия/показа элемента с анимацией.

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

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

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

7 Комментарии

  1. JeStone

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

  2. SelenIT (Автор записи)

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

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

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

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

  3. Алексей

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

  4. Алексей

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

    1. SelenIT (Автор записи)

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

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

      1. Алексей

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

        1. SelenIT (Автор записи)

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

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

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

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

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