От автора: на днях у меня было уникальное задание: создать макет с элементами на всю ширину, при этом один элемент должен оставаться наверху. Это оказалось довольно сложно осуществить, поэтому я документирую данный случай, если кому-то понадобится воссоздать тот же эффект. Часть хитрости была связана с логическим позиционированием на маленьких экранах.
Идея состоит в том, чтобы отображать основной призыв к действию с правой стороны, пока пользователи прокручивают другие разделы на больших окнах просмотра. В меньших окнах просмотра элемент призыва к действию должен отображаться после главного раздела с заголовком «Start your trial».
Здесь есть две основные проблемы:
Создать элементы без полей, которые не мешают липкому элементу
Избежать дублирования HTML
Прежде чем мы рассмотрим несколько возможных решений (и их ограничений), давайте сначала настроим семантическую структуру HTML.
При создании макетов такого типа может возникнуть соблазн создать повторяющиеся разделы с призывом к действию: один для настольной версии, а другой для мобильной версии, а затем, при необходимости, переключить их видимость. Это позволяет избежать необходимости искать идеальное место в HTML и применять CSS, удовлетворяющий обоим требованиям макета. Должен признать, я виноват в том, что время от времени поступаю так. Но на этот раз я хотел избежать дублирования HTML.
Еще нужно учитывать то, что мы используем липкое позиционирование для элемента .box—sticky, что означает, что для правильной работы он должен быть родственником других элементов, в том числе элементов полной ширины. Вот разметка:
<div class="grid"> <div class="box box--hero">Hero Box</div> <div class="box box--sticky">Sticky Box</div> <div class="box box--bleed">Full-bleed Box</div> <div class="box box--bleed">Full-bleed Box</div> <!-- a bunch more of these --> </div>
Давайте создадим липкие элементы
Создание липких элементов в макете сетки CSS довольно просто. Добавляем position: sticky к элементу .box—sticky смещение top: 0, указывающее, где он начинает прилипать. Да, и обратите внимание, что мы делаем элемент липким только в области просмотра больше 768 пикселей.
@media screen and (min-width: 768px) { .box--sticky { position: sticky; top: 0; } }
Помните, что существует известная проблема с липким позиционированием в Safari, когда оно используется с overflow: auto. Это задокументировано на caniuse в разделе известных проблем: Родитель, для которого установлено значение переполнения auto, не сможет работать в Safari с position: sticky.
Хорошо, это было легко. Давайте теперь решим проблему с элементами без полей.
Решение 1. Псевдо-элементы
Первое решение — это то, что я часто использую: абсолютно позиционированные псевдоэлементы, которые растягиваются в обе стороны. Уловка здесь заключается в использовании отрицательного смещения. Если мы говорим о центрированном контенте, то расчет довольно прост:
.box--bleed { max-width: 600px; margin-right: auto; margin-left: auto; padding: 20px; position: relative; } .box--bleed::before { content: ""; background-color: dodgerblue; position: absolute; top: 0; bottom: 0; right: calc((100vw - 100%) / -2); left: calc((100vw - 100%) / -2); }
Короче говоря, отрицательное смещение — это ширина области просмотра, 100vw, минус ширина элемента, 100%, а затем деленная на -2, потому что нам нужно два отрицательных смещения.
Помните, что существует известная ошибка при использовании 100vw, которая также задокументирована на caniuse: В настоящее время все браузеры, кроме Firefox, неправильно считают 100vw всей шириной страницы, включая вертикальную полосу прокрутки, которая может вызвать горизонтальную полосу прокрутки при установке overflow: auto.
Теперь давайте сделаем элементы без полей, когда содержимое не отцентрировано. Если вы посмотрите видео еще раз, обратите внимание, что под липким элементом нет содержимого. Мы не хотим, чтобы наш липкий элемент перекрывал контент, и именно по этой причине у него нет центрированного контента в этом конкретном макете. Сначала мы собираемся создать сетку:
.grid { display: grid; grid-gap: var(--gap); grid-template-columns: var(--cols); max-width: var(--max-width); margin-left: auto; margin-right: auto; }
Мы используем настраиваемые свойства, которые позволяют нам переопределить максимальную ширину, зазор и столбцы сетки без повторного объявления свойств. Другими словами, вместо повторного объявления свойств grid-gap, grid-template-columns и max-width, мы повторно объявляем переменные величины:
:root { --gap: 20px; --cols: 1fr; --max-width: calc(100% - 2 * var(--gap)); } @media screen and (min-width: 768px) { :root { --max-width: 600px; --aside-width: 200px; --cols: 1fr var(--aside-width); } } @media screen and (min-width: 980px) { :root { --max-width: 900px; --aside-width: 300px; } }
Для экранов шириной 768 пикселей и выше мы определили два столбца: один с фиксированной шириной, —aside-width, и второй, заполняющий оставшееся пространство, 1fr, а также максимальную ширину контейнера сетки —max-width.
В области просмотра меньше 768 пикселей мы определили один столбец и зазор. Максимальная ширина контейнера сетки составляет 100% от области просмотра без зазоров с каждой стороны.
Теперь самое интересное. Контент не отображается только на больших окнах просмотра, поэтому расчет не так прост, как вы думаете. Вот как это выглядит:
.box--bleed { position: relative; z-index: 0; } .box--bleed::before { content: ""; display: block; position: absolute; top: 0; bottom: 0; left: calc((100vw - (100% + var(--gap) + var(--aside-width))) / -2); right: calc(((100vw - (100% - var(--gap) + var(--aside-width))) / -2) - (var(--aside-width))); z-index: -1; }
Вместо того, чтобы использовать 100% ширины родительского элемента, мы учитываем ширину зазора и липкого элемента. Это означает, что ширина содержимого в элементах без полей не будет превышать границ главного элемента. Таким образом, мы гарантируем, что липкий элемент не будет перекрывать какую-либо важную информацию.
Левое смещение проще, потому что нам нужно только вычесть ширину элемента (100%), зазор ( —gap) и липкий элемент ( —aside-width) из ширины области просмотра (100vw).
left: (100vw - (100% + var(--gap) + var(--aside-width))) / -2);
Правое смещение сложнее, потому что мы должны добавить ширину липкого элемента к предыдущему вычислению —aside-width, а также зазор —gap:
right: ((100vw - (100% + var(--gap) + var(--aside-width))) / -2) - (var(--aside-width) + var(--gap));
Теперь мы уверены, что липкий элемент не перекрывает контент в элементах без полей.
Вот решение с ошибкой:
И вот решение с исправлением:
Исправление состоит в том, чтобы скрыть переполнение по оси x, что в, любом случае, может быть хорошей идеей:
body { max-width: 100%; overflow-x: hidden; }
Это вполне жизнеспособное решение, и на этом мы можем закончить. Но так не интересно. Обычно есть несколько способов добиться чего-то, поэтому давайте рассмотрим другой подход.
Решение 2. Вычисление отступов
Вместо использования центрированного контейнера сетки и псевдоэлементов мы могли бы добиться того же эффекта, настроив нашу сетку. Начнем с определения сетки, как в прошлый раз:
.grid { display: grid; grid-gap: var(--gap); grid-template-columns: var(--cols); }
Опять же, мы используем пользовательские свойства для определения зазора и столбцов:
:root { --gap: 20px; --gutter: 1px; --cols: var(--gutter) 1fr var(--gutter); }
Мы показываем три столбца в области просмотра меньше 768 пикселей. Центральная колонка занимает как можно больше места, а две другие используются только для создания горизонтального разрыва.
@media screen and (max-width: 767px) { .box { grid-column: 2 / -2; } }
Обратите внимание, что все элементы сетки помещаются в центральный столбец. В области просмотра больше 768 пикселей мы определяем переменную —max-width, которая ограничивает ширину внутренних столбцов. Мы также определяем ширину нашего липкого элемента —aside-width. Опять же, таким образом мы гарантируем, что липкий элемент не будет располагаться поверх какого-либо содержимого внутри элементов без полей.
:root { --gap: 20px; } @media screen and (min-width: 768px) { :root { --max-width: 600px; --aside-width: 200px; --gutter: calc((100% - (var(--max-width))) / 2 - var(--gap)); --cols: var(--gutter) 1fr var(--aside-width) var(--gutter); } } @media screen and (min-width: 980px) { :root { --max-width: 900px; --aside-width: 300px; } }
Далее рассчитываем ширину зазора. Расчет такой:
--gutter: calc((100% - (var(--max-width))) / 2 - var(--gap));
… где 100% — ширина области просмотра. Во-первых, мы вычитаем максимальную ширину внутренних столбцов из ширины области просмотра. Затем мы делим этот результат на 2, чтобы создать зазоры. Наконец, мы вычитаем зазор сетки, чтобы получить правильную ширину столбцов желоба.
Теперь давайте переместим элемент .box—hero так, чтобы он начинался с первого внутреннего столбца сетки:
@media screen and (min-width: 768px) { .box--hero { grid-column-start: 2; } }
Это автоматически подталкивает липкую рамку так, что она начинается сразу после главного элемента. Мы также могли бы явно определить размещение липкой рамки, например:
.box--sticky { grid-column: 3 / span 1; }
Наконец, давайте сделаем элементы без полей, установив значение grid-column 1 / -1. Это указывает элементам начинать содержимое с первого элемента сетки и переходить к последнему.
@media screen and (min-width: 768px) { .box--bleed { grid-column: 1 / -1; } }
Чтобы центрировать контент, мы собираемся вычислить левый и правый отступ. Левый отступ равен размеру столбца с отступом плюс зазор сетки. Правый отступ равен размеру левого отступа плюс еще один зазор сетки, а также ширине липкого элемента.
@media screen and (min-width: 768px) { .box--bleed { padding-left: calc(var(--gutter) + var(--gap)); padding-right: calc(var(--gutter) + var(--gap) + var(--gap) + var(--aside-width)); } }
Вот окончательное решение:
Я предпочитаю это решение первому, потому что оно не использует ошибочные единицы просмотра. Я люблю расчеты в CSS. Использовать математические операции не всегда просто, особенно при объединении разных единиц, например 100%. Понять, что означает 100% — это половина усилий.
Я также люблю создавать сложные макеты, такие как этот, используя только CSS. Современный CSS имеет собственные решения, такие как сетка, липкое позиционирование и вычисления, которые удаляют сложные и несколько тяжелые решения JavaScript. Оставим грязную работу браузеру!
Автор: Silvestar Bistrović
Источник: css-tricks.com
Редакция: Команда webformyself.