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. Это тоже может быть интересно:
Я не понимаю для чего этот избыток скрывающих/показывающих css свойств: display, visibility, box-supress, и к ним в догонку еще html-атрибут hidden.
Мне кажется это только приведет к путанице.
Чем отличается box-supress: hide от visibility: hidden?
По-моему естественней было бы доработать поведение display: initial для того, чтобы учитывался тип элемента, а не ставился inline по-умолчанию, а не вводить еще одно свойство.
Насколько я сам понял, при box-supress: hide элемент рисуется «как бы в уме» — некий аналог documentFragment-а, только не для DOM, а для дерева рендеринга. Он получается не связан с другими выводимыми элементами, но может использоваться для измерения чего-нибудь, например (сейчас иногда для этой цели используют абсолютно позиционированные элементы с visibility: hidden, но это куда больше смахивает на хак). Возможно, я и заблуждаюсь.
К сожалению, это не возможно по определению initial. Возможно, такое получится с revert из нового черновика.
Горизонтальные связи вообще рулят. Ведь мышление у нас ассоциативно, а значит и иерархия, может один из важнейших, но ОДИН, из видов связей, а в том или ином контексте возникают иные связи. И чем больше возможностей описания этих контекстов, тем выразительней язык, тем больше нюансов им можно передать и тем легче он адаптируется к изменениям не теряя структуры.
Правильно ли я понимаю, что во втором примере с таблицей, мы просто для (назначенной) таблицы скрыли иерархию? А иерархие нужна в html, или, вернее, может понадобится для других целей? (например, каких?)
И второе — немного не в тему, но интересно — почему ссылка на скрин для не поддерживаемых бразузеров, такая ахово-большая?
Иерархия осталась в DOM и вполне может пригодиться и в CSS (в примере это тоже показано — разное оформление разных уровней, скрытие/показ подуровней). Но да, сами промежуточные элементы у нас только «в уме», визуально они никак себя не проявляют — как при
display:none
. Главное отличие тут в том, что еслиnone
распространяется и на потомков, у элемента сdisplay:none
и все потомки исчезают, то приdisplay:contents
потомки этого элемента остаются видимыми и добавлются в дерево отрисовки вместо «пропавшего» родителя.А скрины для неподдерживаемых просто заинлайнены прямо в код, в base64 — просто чтоб не заливать их отдельно на хостинг:)
Получается блок с display:contents ставит для своих потомков как бы (математические? или логические) скобки — они объединены и находятся на одном уровне с остальными. С точки зрения же оформления, это похоже на класс.
Имхо, не столько «объединяет» в скобки, сколько, наоборот, «раскрывает» их — убирает обертку и подставляет содержимое напрямую, в «голом» виде.
Вы пишите о симантичности а сами в ссылку поместили h1 и папарграф…
Или это просто для примера…?
Спецификация HTML такое разрешает, один из примеров в ней показывает вообще целую секцию с заголовком, обернутую в ссылку. Семантически это как раз верно, если заголовок — часть единой кликабельной области, логично ему быть внутри ссылки. Другое дело, что скринридеры и т.п. пока плохо умеют передавать такую семантику, поэтому ради доступности действительно лучше не класть больших блоков контента в ссылку — но это проблема не семантики как таковой, а ограничений конкретных технологий доступности (надеюсь, временных). И на момент выхода статьи эта проблема была не настолько известна и актуальна:(