Перейти к основному содержимому

Разработка 22 октября 2026г. 8 мин чтения

Тег picture ломает вёрстку: правило display: contents

Мы обернули больше двухсот картинок сайта в тег picture, чтобы отдавать современный формат WebP с откатом на PNG для старых браузеров. После этого в нескольких местах, которые никто не трогал, тихо поехала раскладка. Разбираемся, почему тег picture меняет поведение вёрстки и как одна строка CSS возвращает всё на место.

Мы обернули больше двухсот картинок сайта в тег picture, чтобы отдавать современный формат WebP с откатом на PNG для старых браузеров. После этого в нескольких местах, которые никто не трогал, тихо поехала раскладка. Разбираемся, почему тег picture меняет поведение вёрстки и как одна строка CSS возвращает всё на место.

Замена img на picture проходит без единой ошибки: скрипт отработал, картинки грузятся, WebP уходит современным браузерам, PNG остаётся запасным вариантом. Поломка находится не по логам, а на глаз: где-то картинка перестала обтекаться текстом, где-то сетка карточек будто сжалась на одну колонку. В консоли пусто. Ошибки нет - есть смысловой сдвиг: раскладку теперь считает не тот элемент.

Зачем вообще нужен тег picture

Тег img принимает один файл. Тег picture - контейнер с несколькими вариантами исходника: внутри лежат правила, какой из них показать. Элементы source задают эти правила: type отвечает за формат (WebP вместо PNG или JPEG), srcset и media - за то, какую картинку выбрать под ширину экрана. Сам img остаётся последним элементом - запасным вариантом, если ни один source не подошёл.

Нам эта конструкция понадобилась ради веса страниц. Часть графики на сайте - плоские двухцветные иллюстрации, а такая графика в WebP сжимается на девяносто с лишним процентов без потери качества. Задача была простая: отдать WebP тем, кто его понимает, а PNG - всем остальным. Ради этого потребовалась именно обёртка, а не замена формата у одного и того же файла.

Что ломается после обёртки и почему тихо

До обёртки между родительским блоком и картинкой не было ничего лишнего: контейнер, а внутри сразу img. Правила CSS, обращающиеся к img напрямую или как к прямому потомку контейнера, видят ровно то, что ожидают.

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

Обтекание текста. Первая картинка в статье блога обтекается текстом слева - float: left на первом дочернем элементе блока. Правило было написано через прямого потомка. После обёртки первым потомком стал picture, а не img, и правило перестало срабатывать: картинка встала блоком, текст ушёл вниз вместо того, чтобы огибать её сбоку.

Выбор первого ребёнка. Там, где вёрстка держалась на выборе первого элемента в блоке, тот же эффект: адресат сместился на уровень выше, потому что первым идёт теперь обёртка.

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

Ни один из этих случаев не даёт ошибки в консоли, потому что синтаксически всё верно. Такую поломку находят не по логам, а прогоняя страницы глазами.

Правило в одну строку: picture { display: contents }

Решение звучит скромно.

picture { display: contents; }

Свойство display: contents убирает у элемента собственный бокс в раскладке, но оставляет содержимое на месте в дереве. Браузер как будто убирает picture из расчёта позиций, и img снова ведёт себя как прямой потомок того же родителя, что и до обёртки. С этой строкой обтекание, грид и флекс возвращаются к прежнему поведению. Правило добавили один раз в общий файл стилей, а не переписывали десятки мест по сайту.

Вторая ловушка той же истории. На сайте есть общий сброс, который делает все картинки блочными - обычная практика, убирающая лишний отступ снизу у img по умолчанию. Формулировка сброса задевала и picture: он получал display: block раньше, чем срабатывало правило про contents, и вся польза от него пропадала. Лечится сужением сброса: он должен касаться img, svg и video, а picture попадать в него не должен.

Чего display: contents не умеет

Прозрачность элемента в раскладке даётся не бесплатно.

Что проверить Что происходит
Поддержка браузеров Chrome, Firefox и Safari - без проблем. Старый EdgeHTML display: contents не понимает, picture там останется блоком
Доступность В старых версиях Safari и Chrome элемент с display: contents исчезал и из раскладки, и из дерева доступности вместе с содержимым
Визуальные свойства У элемента без бокса нет фона, рамки и тени - эти свойства на picture не сработают

Для picture риск по доступности почти нулевой: тег не несёт смысловой роли, вся семантика картинки живёт в alt у img. Но для другого, содержательного элемента та же строка способна незаметно вырезать кусок текста из чтения вслух - см. наш разбор требований WCAG. Если картинке нужна рамка, вешать её приходится на сам img или на родительский контейнер.

Почему "первый ребёнок" перестаёт совпадать

Путаница в этом месте самая частая. display: contents чинит раскладку - позицию элемента в потоке, участие в гриде и флексе. Он не меняет структуру дерева документа и не заставляет CSS-селекторы искать img там, где раньше искали контейнер.

Селектор вида "прямой потомок контейнера и одновременно его первый ребёнок" продолжает искать именно такой img. После обёртки прямой потомок - это picture, а img лежит на уровень глубже. Селектор перестаёт совпадать, и display: contents этого не исправляет: contents отвечает за раскладку, а не за то, кого видит селектор.

Решение - переписать селектор под новый уровень вложенности: первым ребёнком контейнера теперь picture, а дальше зайти на img внутри него. Для обтекания у нас получилось два похожих правила: одно на случай, когда картинка первая сразу в блоке текста, второе - когда она первая внутри абзаца.

Заодно стоит проверить остальные CSS-правила: любой селектор, называющий img через прямого потомка, придётся продублировать так же. Правила без требования прямого потомства - просто вложенный img - трогать не нужно: они находят картинку на любой глубине.

Когда обёртка вообще не нужна

Не все картинки нуждаются в picture: оборачивать всё подряд - это лишняя работа и повод для новой поломки там, где раньше всё держалось.

Логотипы, иконки и другая векторная графика обычно хранятся в SVG. У формата и так минимальный вес, а второй source только добавит разметки без пользы. То же касается декоративных элементов интерфейса вроде стрелок и разделителей.

Смысл есть там, где картинка растровая и заметно тяжелее в PNG или JPEG, чем в WebP: фотографии, скриншоты, иллюстрации, плоская графика с заливками. Такая графика и составила основную массу из двухсот с лишним обёрнутых файлов на нашем сайте.

Чек-лист после массовой замены img на picture

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

  • Проверить страницы каждого типа шаблона отдельно: карточка блога, статья, каталог, лендинг - у каждой свой набор CSS-правил, завязанных на позицию картинки.
  • Смотреть минимум на двух ширинах экрана. Обтекание и грид чаще завязаны на десктопную раскладку, а на мобильном те же блоки часто превращаются в одну колонку и маскируют проблему.
  • Отдельно проверить места с float и первым потомком - самая частая точка поломки.
  • Убедиться, что скрипт массовой обёртки идемпотентен: повторный запуск не должен добавлять второй picture поверх первого. Проверка простая - считать баланс тегов picture вокруг каждого img.
  • Перепроверить страницы, где картинка уже была обёрнута до общей замены: там скрипт мог создать вложенность, если не проверял такие места заранее.
  • После правок прогнать страницы по техническому SEO-аудиту: он тоже проверяет разметку изображений, и вложенный picture там заметен сразу.

Опыт с переводом на WebP показал, насколько вёрстка зависит от точной структуры дерева, а не только от классов. То же самое видно и в разборе брейкпойнтов адаптивной вёрстки: раскладку там часто ломает не размер экрана, а то, какой элемент дерева получает свойства.

Что мы теперь проверяем перед каждой обёрткой картинок

Picture не виноват в поломке: он добавляет в дерево новый элемент, а раскладка на это честно реагирует. Дело было не в теге - в том, что часть CSS-правил держалась на точной позиции img, а не на смысле блока. display: contents закрывает большинство таких случаев одной строкой, но не заменяет ручную проверку селекторов с прямым потомком и не отменяет прогон страниц глазами после массовой правки.

Если у вас картинки ещё в чистом PNG или JPEG и вы планируете переход на WebP, дешевле сразу заложить время на проверку вёрстки на двух ширинах, чем чинить мелочи потом по одной. Мы закладываем такую проверку в техподдержку сайта при любой работе с массовой заменой картинок - если нужна разовая помощь с похожей задачей, пишите, разберём конкретный случай.

Частые вопросы

Обязательно ли оборачивать в picture вообще все картинки на сайте?

Нет. Смысл есть только там, где картинка растровая и выигрывает от смены формата: иллюстрации, фотографии, скриншоты. Иконки, логотипы и другую векторную графику в SVG оборачивать незачем - формат уже компактный, а второй источник ему не нужен.

Почему браузер не показывает ошибку, если вёрстка из-за picture уже сломана?

Потому что синтаксически всё правильно: тег вложен верно, атрибуты на месте, картинка грузится и отображается. Ломается не код, а смысл: раскладку получает не тот элемент дерева. Консоль на такое не реагирует, увидеть проблему можно только глазами на реальной странице.

Можно ли задать тегу picture фон, рамку или отступы?

После display: contents - нет, потому что у элемента больше нет собственного визуального бокса. Background, border, padding и box-shadow на picture в этом случае ничего не дадут. Такие свойства нужно вешать на img внутри picture или на родительский контейнер.

Как не получить вложенные picture, если скрипт массовой обёртки запустить второй раз?

Скрипт должен проверять, не обёрнута ли картинка уже: считать баланс открывающих и закрывающих тегов picture вокруг img и пропускать такие места. Без этой проверки повторный запуск оборачивает уже обёрнутое, и в разметке появляется picture внутри picture с одним и тем же img.

Нужен сайт по этой теме?

Расскажите о задаче – вернёмся с расчётом и предложением структуры.