CSS-live.ru

Парадоксы display:contents и будущее текста в CSS

Внимание: информации, которую вы найдете в этой статье, нет ни на одном зарубежном ресурсе (upd.: за исключением перевода самой этой статьи на английский, спасибо raaly123). Больше того: в самих спецификациях она еще далеко не вся! Сами участники рабочей группы CSS всё еще чешут в затылках, решая, что же именно туда написать. Может показаться, что речь о каких-то теоретических, умозрительных планах на дальнее будущее, не связанных с браузерной реальностью — но нет: то, о чем пойдет речь, работает как минимум в двух браузерах (и даже более-менее единообразно).

Заинтригованы? Если да — добро пожаловать в статью и узнайте, как вроде бы пустяк, о котором сразу не подумали, едва не перевернул вверх дном одно из базовых понятий CSS. Впрочем, в любом случае заходите, если что, похвастаетесь «тайным знанием» перед коллегами, которые статью не читали:)

Еще раз вкратце про display:contents

Значение display:contents нам уже знакомо. Если вы до сих пор игнорируете его, то зря: есть немало причин, по которым оно может стать очень важным. Во-первых, оно — одно из лучших решений проблемы отсутствующих подсеток в гридах, возможность не «калечить» разметку ради запихивания разнородных блоков в один грид, способ расширить возможности оформления, не ломая семантики (а значит, и доступности). Во-вторых, display:contents — стиль по умолчанию для элемента <slot> из новой спецификации теневой DOM, а это основа грядущих нативных веб-компонентов. И в-третьих: если раньше оно работало лишь в одном браузере (Firefox), то теперь работает и в Chrome. Пока за флагом «Экспериментальные функции веб-платформы», но работает (хотя canIUse и MDN старательно замалчивают этот факт!:). А для упомянутого элемента <slot> — еще и в Safari (обновлено 12.01.2018: уже не только для <slot>!). Это не единственные причины, но, согласитесь, даже их достаточно, чтобы приглядеться повнимательнее.

По описанию, действие этого значения выглядит непривычно, но логично. Потомки элемента становятся непосредственно на его место, будто они — потомки его родителя, а сам элемент будто исчезает. Внутри потомков всё по-старому. Только вложенность как будто изменилась. Вроде, ничего сложного.

Но это лишь до тех пор, пока в игру не вступят…

«Необычные» элементы и display:contents

Что будет, если задать display:contents замещаемому элементу, типа <iframe> или <object>? Или элементу формы, типа <textarea> или <button> (на всякий случай: в HTML элементы форм явно отнесены к незамещаемым элементам)? А что будет с svg-элементами типа <use>?

С пустыми элементами, типа <img>; и <input>, по идее, всё более-менее ясно: нет контента — нет и потомков, которых можно поднять на какой-то уровень выше, так что display:contents для них эквивалентно display:none (хотя с учетом теневой DOM, как показал пример незагруженных <img> в статье про хитрые псевдоэлементы, даже это не столь однозначно). Но у <object>, <textarea> и <button> контент есть, так что показать есть что. Но контент <object>-а появляется лишь если основная функциональность не сработала, а в контенте <textarea> мало смысла, если он не редактируется, значит, и показывать их не надо. Как же быть?

Об этом Таб Аткинс, предложивший display:contents для модуля CSS Display 3-го уровня, видимо, не подумал. А потом он и другие редакторы спецификации, вместе с разработчиками браузеров, долго ломали головы. В итоге пришлось сделать как обычно: добавить в спецификацию отдельное приложение и там «в лоб» перечислить, как ведет себя с ним каждый из таких хитрых элементов.

В сухом остатке получилось, что большинство элементов форм и «замещанцев» — не только пустых (<img>, <input>, <embed>), но и таких, в отображении которых замешана «особая браузерная магия» (<object>, <textarea>, <iframe>, <video>, <canvas>, <select> и еще несколько) скрываются целиком (как при display:none). У <legend> содержимое выводится, но отображается как обычный текст — без фокусов со встраиванием в разрыв рамки. Содержимое <button>, <fieldset> и <details> тоже просто отображается, как есть — без «спецэффектов», применяемых к этим элементам по умолчанию.

Так что c display:contents появилась надежда одолеть нелогичное ограничение <button> — невозможность задать ему display:inline. Кнопка упрямо остается «вещью в себе», как при inline-block. Многие верстальщики, которых жизнь заставляла оформить <button> и <a> в одинаковом стиле (оставим в стороне холивор, антипаттерн ли это), проклинали эту особенность, порой отказываясь в итоге от правильного <button> в пользу костылей а-ля <a href="#">. Теперь наконец можно будет выводить содержимое кнопки, как обычный текст — без всяких «волшебных» рамок. Но этот текст останется нажимаемым и будет отправлять форму/выполнять действие — ведь display:contents влияет только на отображение, не затрагивая DOM! Более того, в Chrome (58+ с включенным флагом) это уже работает!

скриншот примера с текстовой кнопкой в Chrome Canary

В Firefox, к сожалению, «магия» кнопки еще сильна… но в последнее время ее понемногу усмиряют, так что и эта проблема должна решиться (обновлено 16.03.2018: в Firefox 59+ пример уже тоже работает!).

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

Из SVG-элементов отличились <g>, <tspan> и <use>. На их месте будет отображаться их содержимое (вместо группы — набор отдельных элементов группы, вместо составной надписи — отдельные текстовые элементы). Случай с <use> вдвойне интересен: на его месте отображается не «истинное» содержимое — которого у него нет — а теневое (копия подключенного элемента, как будто его на самом деле туда перенесли). У всех остальных элементов, хоть SVG, хоть HTML, теневое содержимое игнорируется! Для прочих же SVG-элементов — включая корневой <svg> — display:contents равносилен display:none, скрывает их полностью.

Уфф. С непростыми элементами худо-бедно разобрались. Но тут подкрался следующий сюрприз…

Наследование и display:contents

Да, у display:contents и с обычными элементами сюрпризов хватает. Как вы думаете, что получится в результате этого примера?

<div>
 <p>
   <span>Раз</span>
 </p>
 <span>Два</span>
</div>
div {
 color: blue;
}
p {
 display: contents;
 color: red;
}

Какого цвета должна быть надпись «Раз»? Красного, «по наследству» от <p>? Но ведь в визуальной структуре, в дереве рендеринга, этот <p> не участвует, в ней оба <span>-а оказываются на одном уровне, непосредственно внутри <div>-а. Выходит, синего?

К счастью, здесь и спецификация, и браузеры единодушны: наследование в CSS всегда идет по DOM-дереву (есть редкие исключения, например, для псевдоэлемента ::first-line и анонимных боксов, но речь не о них). Если какой-то уровень иерархии невидим — не беда, наследоваться от него это не мешает. В конце концов, наследуются же всякие border-spacing’и таблиц от нетабличных предков (что очень кстати для оформления разметки, генерируемой пользователями). Так что текст будет красным.

Просто? На первый взгляд, да. До тех пор, пока не понадобится выяснить, как должны себя вести…

Свойства текста и display:contents

Следующий пример с виду еще проще предыдущего:

<section>
 <div>Текст</div>
</section>
section {
 color: red;
}
div {
 color: green;
 display: contents;
}

Какого цвета надпись «Текст»? Это не элемент, это анонимный элемент строки (inline box). Спецификация раньше гласила, что эти штуки наследуют свойства «предков в дереве рендеринга». Див в дерево рендеринга не попадает, текст там напрямую вложен в <section>. Выходит, он красный? Но что если чуть изменить разметку?

<section>
 <div><span>Foo</span></div>
</section>

Элемент <span>, как мы уже знаем, наследует свойства от дива (даже если тот никак не отображается сам), так что в этом случае текст однозначно зеленый.

Из-за этого простенького примера сами редакторы спецификации едва не переругались. Элика Этемад (более известная как fantasai) решила, что да, в первом примере текст должен быть красным. Но Таб Аткинс посчитал, что текст, по волшебству перекрашивающийся от добавления простого, никак не оформленного элемента — это что-то жутковатое (и я его очень понимаю:).

Но если в обоих случаях текст зеленый (как это, кстати, реально происходит в Firefox и Chrome с включенным флагом) — что же тогда наследует этот цвет в первом случае? Сам анонимный элемент строки (anonymous inline box)?

Допустим. Но как тогда быть со следующим примером?

div {
 display: flex;
 flex-direction: column;
}
span {
 color: red;
 display: contents;
}
<div>Раз<span>Два</span></div>

Сколько тут должно быть флекс-элементов? Один (текст из <span>-а и текст его родителя войдут в общий анонимный элемент строки)? Но как тогда строка «Два» унаследует цвет? Два (каждый из разноцветных кусков текста — отдельный анонимный элемент строки, который станет отдельным флекс-элементом)? Но тогда результат будет таким же, как и без display:contents, содержимое <span>-а останется в его рамках, визуальная иерархия не изменится. А ведь смысл всей затеи — как раз в возможности ее менять!

К счастью, браузеры единогласно отображают обе строки как единый флекс-элемент (или как единый грид-элемент, если поменять display контейнера на grid — общая логика у этих схем раскладки схожа). То есть «Раз» и «Два» всё-таки попадают в один элемент строки. Но стили-то у них явно разные!

Вот такой парадокс.

Справедливости ради, по-разному оформленные фрагменты текста в одном элементе строки встречались в CSS и раньше. Например, если какого-нибудь символа не оказывалось в основном шрифте и он отображался шрифтом по умолчанию: тогда при line-height: auto его фактическое line-height (то, что получается после перемножения метрик шрифта на font-size и используется при выводе на экран) запросто могло отличаться от соседей. Но указанные значения всех CSS-свойств в пределах одного элемента строки (тем более анонимного) до прихода display:contents всегда бывали одинаковыми.

И вот возник необычный прецедент. У нас появился некий абсолютно новый объект «кусок текста» (run of text)…

  • с которым умеют успешно работать браузеры;
  • который умеет самостоятельно наследовать свойства от родительского элемента;
  • который не является CSS-боксом (ни обычным, ни анонимным);
  • и который… не описан ни в одной спецификации!

К такому повороту редакторы спецификаций готовы не были. Ведь новый статус текста — не «бедного родственника», а полноправного участника DOM-дерева — нужно было отразить как минимум:

  • в самом модуле CSS Display — указать, что в DOM есть не только элементы, но и текст;
  • в модуле CSS Cascade — определить правила наследования для него;
  • в модуле CSS Scoping — уточнить, что происходит с текстом в теневом дереве (снова веб-компоненты!);
  • в модуле CSS Text — надо же, в конце-то концов, более строго определить само это понятие;
  • в модуле CSS Text Decoration — прояснить нюансы наследования текстового оформления.

И это только то, что Таб Аткинс вспомнил сходу. Так что скорее всего — еще в куче мест.

И все перечисленные спецификации уже вовсю правят. Хочется надеяться, что это поможет прояснить и другие странности CSS, связанные с текстом. Вот как оно бывает в CSS: захотели добавить всего одну простую, казалось бы, вещь — а в итоге как бы нам всем не пришлось учить CSS заново!

А говорят еще — спецификации, мол, скучные..:)

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

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

  1. Хорошая статья! И хорошо что авторы спецификации продолжают ее дорабатывать.

    В последнем примере с цветом текста. Вариант наследования выглядит более логичным( display: contents ~ display: somethink), иначе с игнорирование цвета текущего елемента ( display: contents ~ display: none) придется игнорировать почти все прочие свойства применяемые к элементу с display: contents или вводить кучу исключений.

  2. Ужасный подход к верстке.
    Как веб-разработчик с 12-летним стажем я бы рекомендовал никогда не ипользовать такое св-во.

    1. Хорошо бы услышать аргументацию! :)

      Примеры в этой статье, конечно, намеренно утрированные, чтобы показать граничные случаи. И любая дополнительная возможность менять представление элемента — это дополнительный риск «выстрелить себе в ногу». Но случаев, где display:contents может очень выручить, тоже немало (не говоря о слотах для теневой DOM, где без него вообще никак), и я, как веб-разработчик с 2003 года, предпочту использовать его, чем надругаться над разметкой. Другое дело, что ничего (включая модные новинки) никогда нельзя использовать бездумно.

      1. Этот подход столь же маразматичен, как и использование SASS/LESS.

        Многие с пеной у рта будут доказывать, что SASS/LESS очень полезны и без них нельзя обойтись.
        Но моё мнение (мнение разработчика с 12-летним опытом), что SASS/LESS — это костыли и их применение — это следствие безграмотной разработки CSS-иерархии внутри проекта.

        Как правило на SASS/LESS «ушлые» разработчики просто переносят «тонну» стиля из одного элемента на другой элемент в результате чего на выходе получается «полная дичь», «каша» и «мусор» — многократное дублирование одних и тех же стилей…

        Почему так происходит?
        Приходят мокапы от дизайнера…
        На начальном этапе не все мокапы отрисованы.
        В результате фронтендер начинает «пилить» то что уже известно при этом не особо разрабатывая иерархию наследования стилей (это не удивительно, ведь верстальщик редко когда бывает дипломированным инженером и ему просто не ведомы принципы грамотного проектирования и наследования).
        Потом «прилетают» новые мокапы и о чудо — где то что то поменялось и появились новые элементы…
        Переписывать гору CSS просто лень и начинается «тупой» перенос существующих стилей на всё, что возможно…

        При этом, те, кто используют SASS/LESS обычно приводят в пример возможность создания переменных, что я считаю полезной вещью, но при всём при этом, эти «деятели» используют эту возможность в последнюю очередь…

        CSS в свой сути поддерживает иерархии (каскадность).
        Я грамотно разбиваю всё на блоки и на каждый блок вешаю свой CSS.
        Создаю общий CSS для общих стилей.
        Всё работает идеально.
        Использующих SASS/LESS считаю недо-верстальщиками, низшей кастой…

        1. Использующих SASS/LESS считаю недо-верстальщиками, низшей кастой…

          ЧСВ зашкаливает?

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

          Как правило на SASS/LESS «ушлые» разработчики просто переносят «тонну» стиля из одного элемента на другой элемент в результате чего на выходе получается «полная дичь», «каша» и «мусор»

          Говнокодер он и на чистом CSS будет говнокодить и дублировать тонны кода.

      2. Хотите от меня примера грамотного решения?
        Использование шаблонов!
        В системе имеются шаблоны (например Drupal или другая система).
        Требуемый шаблон просто модифицируется или создаётся новый и используется для заданного элемента на всём сайте или только в определенном месте.
        Всё. Никакого маразма — на фронтенд «прилетает» идеальная разметка и для неё остается сделать простой и понятный стиль.

        1. А если один и тот же элемент должен по-разному выглядеть на разных разрешениях (напр. на узких экранах как список, на средних как таблица, на больших как плитка)? Делать разные «идеальные» шаблоны на сервере и перезагружать страницу при каждом ресайзе окна? :)

          1. Есть 2 подхода:

            1) под разные типы media (мобильник, ПК), делаются разные шаблоны.
            сайты разносятся на разные домены, например example.com, m.example.com.
            стили и разметка для них различна.
            блоки и информация в них тоже может отличатся.

            2) попытка сэкономить и сделать 1 шаблон для всего — разметка одна, стилей много.
            при сжатии страницы, блоки просто прячутся, но при этом всё-равно грузятся.
            стили постоянно глючат или близки к этому.
            код стилей и всего остального представляет из себя мусорку…

            первый случай — вершина качества (когда делаешь для себя)
            второй — вариант от безысходности (когда делаешь массово на заказ)

            p.s.1 то, что вы «клепаете» как на фабрике по второму варианту еще не означает, что ваш подход правильный… он правильный в плохом варианте разработки (от безысходности)…

            p.s.2 на мобильнике размер экрана маленький и врядли в момент исполнения страницы вдруг маленькая страница станет большой, аналогично и с ПК — большая страница врядли станет размером с мобильник, а если даже и станет, то нет необходимости ужимать контент ведь есть полосы прокрутки, поэтому что то там прятать просто нет смысла

  3. Интерестная статья!
    В начале сказано что если кто-то готов перевести на английский было бы здорово — этот кто-то: я! Если никто еще не взялся за перевод, сообщите, I will happily do it! :D

    1. Здравствуйте, Екатерина! Спасибо за проявленный интерес к статье. По поводу перевода на английский: да, конечно же мы не против. Мы были бы очень рады, если бы вы сделали это! :)

      Екатерина, а ещё, если не сложно, свяжитесь со мной по почте (psywalker09@gmail.com), есть один вопросик:)

  4. Статья интересная, то, что такая простая логика, как в display: contest может иметь столько подводных камней даже в голову не приходило!

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

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

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

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