Убираем сдвиги в верстке наложением в CSS Grid

Перевод статьи Prevent layout shifts with CSS grid stacks с сайта www.hsablonniere.com для css-live.ru, автор — Юбер Саблоньер

Люди используют CSS Grid по двум причинам:

  1. CSS — это потрясающе! Факт, как ни крути.
  2. Гриды — отличный инструмент для создания сложных двумерных макетов.

У меня иногда бывает третья причина использовать CSS Grid: предотвратить сдвиги в верстке. Я пытался придумать для этого приема прикольное сокращение, но у меня получилось лишь АСПНГ: «АнтиСдвиговый Прием с Наложением в Гридах». Вряд ли у меня получится похвастаться мастерством в «изобретении технических терминов» в резюме на LinkedIn, так что жду ваших предложений получше.

Давайте я объясню прием на реальных примерах. В этой статье я покажу:

  • Реальную проблему сдвигов в верстке, с которой я столкнулся в работе над одним компонентом.
  • Ограничения решения с абсолютным позиционированием.
  • Преимущества решения с гридом.

Погодите, о каких вообще сдвигах идет речь?

Сдвиг в верстке — то, что происходит, когда обновление на странице заставляет что-то существенно переместиться. Чаще всего сдвиги в верстке происходят во время загрузки страницы, пока контент отрисовывается постепенно. Например, когда в страницу вставляется картинка с неизвестной высотой, прямо перед абзацем, который вы читаете, текст куда-то убегает, и это порядком раздражает. Еще такое часто бывает, когда сторонние скрипты (рекламные или партнерские сервисы) добавляют на страницу новые неизвестные элементы.

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

Для начала — немного контекста

Я работаю в Clever Cloud, это платформа автоматизации IT. Наши клиенты выкладывают свой код и мы делаем за них всё остальное: билдим, деплоим, хостим, масштабируем, поддерживаем, восстанавливаем и т.д. Когда им нужно настраивать свои приложения, базы данных и прочие сервисы, они используют наш веб-интерфейс: «консоль». Большую часть времени я провожу над этим проектом, и выглядит он примерно так:

скриншот Clever Cloud

На скриншоте выше — информация о моем собственном сайте, приложении на Node.js, которое хостится на нашей платформе. Справа вы видите круговую диаграмму, показывающую распределение кодов HTTP-ответов, отданных приложением за последних 24 часа.

диаграмма распределения кодов HTTP-ответов

Когда я работал над этим компонентом, я старался не усложнять. Я представил себе, что наши пользователи и так поймут, что диаграмма относится к текущему приложению. Чтобы не загромождать экран, я решил убрать подробности типа точного количества запросов в подсказки, доступные с помощью мышки или касания. Но, визуально «очистив» диаграмму, я столкнулся с проблемой. Как объяснить следующие нюансы?

  • Диаграмма показывает только данные за последние 24 часа.
  • Каждый пункт в легенде можно кликнуть и показать/скрыть разные категории кодов статуса. Спасибо, Chart.js!

Доступного пространства слишком мало, чтобы добавить подробный заголовок вверху или внизу диаграммы. Так что, чтобы помочь пользователям узнать это, я добавил кнопку дополнительной информации (ℹ️) в правый верхний угол. При клике по этой кнопке диаграмма прячется, и вместо нее показывается краткий текст. Выглядит это примерно так:

краткий текст, поясняющий диаграмму

По клику на кнопку «Закрыть» прячется этот краткий текст и диаграмма показывается снова. Реализовать поведение такого переключателя не так уж трудно. Возьмем упрощенную версию шаблона:

<div class="chart-component"> <div class="title"> Коды ответов HTTP <button>перекл. диаграмма/пояснение</button> </div> <div class="chart"> <!-- диаграмма здесь --> </div> <div class="info"> <!-- краткий текст здесь --> </div>
</div>

В этом HTML я могу по клику на кнопке просто переключить состояние компонента между  .chart и .info, и скрыть нужную панель с помощью display: none.

Проблемы сдвига в верстке

В этом подходе есть большая проблема, в нем случаются сдвиги в верстке.

Почему так?

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

  • Неизвестной ширины компонента, зависящей от контекста, в котором он используется
  • Длины краткого текста, которая зависит от языка (английский либо французский)

В итоге, когда компонент содержит диаграмму, он оказывается чуть меньше по высоте, чем когда содержит краткий текст.

Два состояния одного UI-компонента рядом: слева круговая диаграмма, справа краткий текст. Английская версия.

С французским текстом проблема еще острее, потому что он чуть длинее:

Два состояния одного UI-компонента рядом: слева круговая диаграмма, справа краткий текст. Французская версия.

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

  • В лучшем случае другие части верстки слегка сместятся.
  • В худшем случае кнопка под курсором мышки сдвинется и под ним окажется что-то другое (привет, кнопка поиска в Твиттере).

Видео внизу показывает, что происходит при переключении между состояниями.

Можете посмотреть на первый сдвиг верстки, когда я перехожу с «диаграммы» на «информацию». Соседняя гистограмма сдвигается вниз и карта увеличивается.

Теперь смотрите, что происходит, если я прокручу страницу вниз и вернусь к состоянию «диаграмма». Следите за курсором мышки. Я навел его на кнопку и больше не двигаю, но простой клик вызывает цепную реакцию:

  • Высота компонента уменьшается до размера диаграммы.
  • Высота всей страницы уменьшается.
  • Вся страница чуть-чуть проскролливается вверх.
  • Курсор мышки оказывается уже не над кнопкой, а над текстом.

Как нам теперь этот сдвиг предотвратить?

Решение с абсолютным позиционированием

До появления CSS Grid я бы решил эту проблему с помощью CSS-свойства position.

Первым делом я использовал бы visibility: hidden вместо display: none для скрытия «неактивной» панели:

Два состояния одного UI-компонента представлены рядом. Слева диаграмма, справа краткий текст.

Как видите, с visibility: hidden «неактивная» панель скрыта, но компонент так рассчитывает свой размер, как будто обе панели на месте. Теперь у меня компонент стабильной высоты, с учетом высоты и .chart, и .info.

Затем я бы применил position: absolute для .chart. Это убирает элемент из нормального потока. Что означает, что компонент будет рассчитывать свою высоту так, как будто .chart нет вообще. Таким образом, высота компонента будет зависеть главным образом от размера .info, а именно это мне и нужно.

Останется только задать .chart точно такие же положение и размер, как у .info. Использование position: absolute означает, что .chart позиционируется «относительно своего ближайшего позиционированного предка». То есть предка с любым значением position кроме дефолтного (static). Чаще всего я для этого применяю position: relative у родительского элемента. В этой ситуации мне понадобится дополнительная обертка .wrapper вокруг обеих панелей:

<div class="chart-component"> <div class="title"> Коды ответов HTTP <button>перекл. диаграмма/информация</button> </div> <div class="wrapper"> <div class="chart"> <!-- диаграмма здесь --> </div> <div class="info"> <!-- краткий текст здесь --> </div> </div>
</div>

Для такого HTML решение в CSS будет выглядеть примерно так:

.wrapper { /* .wrapper — ближайший позиционированный предок для .chart */ position: relative;
} .chart { /* .chart убран из нормального потока, поэтому у .wrapper точно такой же размер (и положение), как у .info потому что .info — единственный потомок, оставшийся в потоке .chart позиционируется относительно .wrapper */ position: absolute; /* то же положение, что у .wrapper (а значит, и у .info) */ left: 0; top: 0; /* тот же размер, что у .wrapper (а значит, и у .info) */ height: 100%; width: 100%;
}

Это решение с position: absolute помогает добиться моей цели. Размер компонента теперь всегда зависит от размера .info, даже когда он скрыт, а отображается .chart.

Больше нет никаких сдвигов при переключении состояний, но остались ограничения:

Во-первых, пришлось добавить .wrapper с position: relative ради одного лишь position: absolute для .chart. CSS потрясающий, но овладеть его хитростями бывает непросто. Если у вас до сих пор бывают неясности с CSS-свойством position, этот подход может испугать, но не беспокойтесь:

  • Это нормально. Вы не в этом не одиноки!
  • Со временем и практикой станет полегче…

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

Теперь посмотрим, как гриды справятся с этим лучше.

Решение на гридах

CSS Grid уже поддерживается во всех «вечнозеленых» браузерах (т.е. автоматически обновляемых — прим. перев.). А значит, можно рассчитывать на возможность помещать несколько элементов в одну грид-область и накладывать их друг на друга.

Что вы имеете в виду под грид-областью?

Если вы не очень знакомы с CSS Grid, то, прежде чем читать статью дальше, я бы посоветовал:

Теперь, когда вы лучше знакомы с CSS-гридами, можно поговорить о теперь уже знаменитом «АнтиСдвиговом Приеме с Наложением в Гридах» (уж простите). Возьмем наш изначальный простой шаблон:

<div class="chart-component"> <div class="title"> Коды ответов HTTP <button>перекл. диаграмма/пояснение</button> </div> <div class="chart"> <!-- диаграмма здесь --> </div> <div class="info"> <!-- краткий текст здесь --> </div>
</div>

Если мы применим CSS Grid для .chart-component вот так:

.chart-component { display: grid; gap: 1rem;
}

то наш компонент будет выглядеть вот так:

UI-компонент показывает диаграмму и краткий текст в виде одноколоночного грида в инспекторе CSS-гридов Firefox

Благодаря чудесному инспектору CSS-гридов из отладчиика Firefox у нас есть подписи для рядов и колонок. Мы видим, что:

  • .title, .chart и .info находятся между 1 и 2 линиями колонок
  • .title — между 1 и 2 линиями рядов
  • .chart — между 2 и 3 линиями рядов
  • .info — между 3 и 4 линиями рядов

Это потому, что поведение по умолчанию — простая одноколоночная сетка: дочерние элементы размещаются по порядку. С помощью grid-column и grid-row можно заставить элементы разместиться в определенной области грида.

Например, если мы вот так разместим наши два элемента в одной и той же области:

.chart,
.info { grid-column: 1 / 2; grid-row: 2 / 3;
}

то результат будет выглядеть так:

UI-компонент показывает диаграмму и краткий текст наложенными поверх друг друга в инспекторе CSS-гридов Firefox

Если вас что-то смущает, не забывайте, что нужная панель будет скрыта с помощью visibility: hidden.

Этим приемом мы фактически велели CSS-движку подготовить сетку, в которой область между 1 / 2 линиями колонок и 2 / 3 линиями рядов (т.е. ячейка на пересечении первой колонки и второго ряда — прим. перев.) должна подстраиваться под то, что у нее внутри. Другими словами: эта самая область всегда будет такой величины, как большее из .chart and .info. Таким образом, высота всего компонента при переключении состояний будет неизменна.

Больше никаких сдвигов!

В сравнении с решением на абсолютном позиционировании, мы улучшили ситуацию:

  • Не нужна добавочная обертка .wrapper
  • Будет прекрасно работать и более чем с двумя панелями
  • Незачем гадать, какая панель будет управлять размером всего компонента

А теперь, бонусом, позвольте показать вам еще один реальный пример, где я использовал этот прием.

Предотвращение горизонтальных сдвигов

В предыдущем примере мы разбирались с высотой компонента, но этот прием можно использовать и для ширины. Если еще раз взглянуть на консоль Clever Cloud в целом, то в правом верхнем углу вы заметите несколько кнопок для управления состоянием приложения.

Скриншот веб-консоли Clever Cloud, показывающей обзор приложения Node.js

У кнопки «Stop app» («Остановить приложение») горизонтальные внутренние отступы больше, чем у остальных. Это потому, что при клике на эту самую кнопку текст на 3 секунды меняется на «Click to cancel» («Нажмите для отмены»), на случай, если вы запаниковали передумали.

Если бы не прием с гридом, изменение ширины при смене текста кнопки со «Stop app» на «Click to cancel» сдвинуло бы все остальные кнопки вот так:

Две строки из четырех кнопок рядом друг с другом, разной ширины

На узких экранах перенос кнопок на новую строку мог бы вызвать еще больший сдвиг вёрстки, типа такого:

Благодаря «АнтиСдвиговому Приёму с Наложением в Гриде» (пожалуйста, пришлите свои идеи для названия, я уже не могу) не нужно гадать, в каком состоянии кнопка больше. Какова ни была бы длина у нормального текста и текста для отмены, кнопка всегда будет достаточного размера.

Две строки из четырех кнопок одной и той же ширины рядом друг с другом

Больше никаких сдвигов!

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

Полезные ссылки

Про CSS и гриды:

Компонент круговой диаграммы и компонент кнопки — части библиотеки компонентов Clever Cloud. Код открыт на Гитхабе и документация (с наглядным предпросмотром) опубликована с помощью Storybook.

Подробнее о компонентах, о которых мы говорили, можно узнать здесь:

  • Chart.js: JavaScipt-библиотека, использованная для этих диаграмм
  • <cc-tile-status-codes>: компонент круговой диаграммы
  • <cc-button>: компонент кнопки с «нажмите для отмены»

Благодарности

Спасибо вам, замечательные рецензенты, за уделенное время: Жюльен Дюрийон, Александр Берто, Энтони Рико, Сара Хаим-Любчански, Жюльен Ленгран-Ламбер и Ральф Д. Мюллер.

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