Мы обернули больше двухсот картинок сайта в тег 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, дешевле сразу заложить время на проверку вёрстки на двух ширинах, чем чинить мелочи потом по одной. Мы закладываем такую проверку в техподдержку сайта при любой работе с массовой заменой картинок - если нужна разовая помощь с похожей задачей, пишите, разберём конкретный случай.

