Fluid-верстка на clamp() и CSS Container Queries: как раз и навсегда забыть про сотни @media брейкпоинтов в компонентах Битрикса
Последние записи
Хостинг для сайта
Если выбираете хостинг под проект — советую Timeweb. Современная инфраструктура, широкий выбор тарифов от стартовых до VPS под нагрузку, отзывчивая техподдержка, которая отвечает по существу, а не отписками. Я сам держу на нём часть своих проектов и рекомендую клиентам.
Перейти Партнёрская ссылка. Для вас цена не меняется.Почему классический адаптив в Битриксе превращается в кашу
Откройте любой коммерческий шаблон Bitrix, которому больше двух лет. В папке style.css каждого компонента — десятки, а то и сотни строк вида @media (max-width: 768px), @media (max-width: 480px), @media (min-width: 1200px). Карточка товара прописана отдельно для каталога, отдельно для слайдера «Вы смотрели», отдельно для мобильной версии. Перенесли карточку из широкой сетки в узкий сайдбар — верстка поехала, потому что она знает только про ширину экрана, но ничего не знает про ширину родителя.
Это не проблема конкретного проекта. Это системная проблема подхода, при котором компонент проектируется с оглядкой на вьюпорт, а не на контекст, в котором он реально живёт. И пока мы продолжаем писать медиа-запросы под каждый чих, будем перекладывать одни и те же костыли из компонента в компонент.
Часть 1. clamp() — жидкие значения без единого брейкпоинта
Функция clamp() принимает три значения: минимум, предпочтительное значение и максимум. Между минимумом и максимумом значение плавно интерполируется в зависимости от ширины вьюпорта. Никаких скачков, никаких «на 767px одно, на 768px другое».
font-size: clamp(1rem, 2.5vw, 2rem);
Здесь 1rem — это минимальный размер шрифта. 2rem — максимальный. 2.5vw — предпочтительное значение, которое масштабируется вместе с вьюпортом. Функция вернёт max(1rem, min(2.5vw, 2rem)), то есть не даст тексту стать меньше 1rem или больше 2rem, но между этими границами он будет масштабироваться плавно.
Как считать формулы руками
Вручную считать clamp() для каждого размера — муторно. Формула такая: наклон slope = (maxSize - minSize) / (maxVw - minVw), коэффициент vw — это slope * 100, свободный член — minSize - slope * minVw. Итоговое выражение выглядит как clamp(minSize, intercept + (slope * 100)vw, maxSize).
Пример: шрифт должен быть 16px на ширине 320px и 24px на 1200px. Наклон: (24 - 16) / (1200 - 320) = 0.00909. Коэффициент vw: 0.00909 * 100 = 0.909. Свободный член: 16 - 0.00909 * 320 = 13.09px ≈ 0.818rem. Результат: clamp(1rem, 0.818rem + 0.909vw, 1.5rem).
Ручной расчёт хорош для понимания, но в работе лучше использовать готовые инструменты или SASS/LESS-миксины, которые генерируют эти формулы прямо в сборке. Если вы работаете с SASS, миксин занимает десяток строк и избавляет от ручной арифметики.
Жидкая типографика в шаблоне сайта
Начнём с общего шаблона. В Bitrix шаблон сайта — это header.php, footer.php и template_styles.css. Определим жидкие размеры шрифтов и отступов через CSS-переменные, чтобы они были доступны во всех компонентах.
:root {
--font-size-xs: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem);
--font-size-sm: clamp(0.875rem, 0.8rem + 0.35vw, 1rem);
--font-size-base: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
--font-size-lg: clamp(1.25rem, 1.1rem + 0.75vw, 1.75rem);
--font-size-xl: clamp(1.75rem, 1.4rem + 1.5vw, 2.75rem);
--font-size-2xl: clamp(2.25rem, 1.5rem + 3vw, 4rem);
--space-xs: clamp(0.25rem, 0.2rem + 0.25vw, 0.5rem);
--space-sm: clamp(0.5rem, 0.4rem + 0.5vw, 1rem);
--space-md: clamp(1rem, 0.8rem + 1vw, 1.5rem);
--space-lg: clamp(1.5rem, 1rem + 2vw, 3rem);
--space-xl: clamp(2rem, 1.5rem + 3vw, 5rem);
}
Дальше в template_styles.css применяем эти переменные к базовым элементам.
body {
font-size: var(--font-size-base);
line-height: 1.6;
}
h1 { font-size: var(--font-size-2xl); }
h2 { font-size: var(--font-size-xl); }
h3 { font-size: var(--font-size-lg); }
.container {
max-width: 1280px;
margin-inline: auto;
padding-inline: var(--space-md);
}
Никаких медиа-запросов для типографики. Заголовки, тексты и отступы масштабируются плавно от 320px до 1920px. Медиа-запросы остаются только для смены структуры: показать/скрыть сайдбар, переключить меню на бургер.
Каркас страницы: футер внизу, сайдбар сбоку, контент первым
Общая раскладка страницы — это как раз то место, где медиа-запросы уместны: структура меняется принципиально, а не плавно. Но сами размеры колонок и отступы внутри них — жидкие.
.page {
display: grid;
grid-template-rows: auto 1fr auto;
min-height: 100vh;
}
.page--has-sidebar {
grid-template-columns: 1fr 320px;
grid-template-areas:
"header header"
"content sidebar"
"footer footer";
}
.page--has-sidebar-left {
grid-template-columns: 320px 1fr;
grid-template-areas:
"header header"
"sidebar content"
"footer footer";
}
.page--header { grid-area: header; }
.page--content { grid-area: content; }
.page--sidebar { grid-area: sidebar; }
.page--footer { grid-area: footer; }
Ключевой момент: в разметке контентная область должна идти перед сайдбаром, чтобы при линейном чтении (скринридером, парсером, в режиме без CSS) основной контент был доступен первым. grid-template-areas позволяет расположить его визуально справа или слева независимо от порядка в HTML.
На мобильных сайдбар уходит вниз или скрывается — это уже структурное изменение, и здесь медиа-запрос оправдан.
@media (max-width: 1024px) {
.page--has-sidebar,
.page--has-sidebar-left {
grid-template-columns: 1fr;
grid-template-areas:
"header"
"content"
"sidebar"
"footer";
}
}
Обратите внимание: медиа-запрос управляет структурой, а не размерами. Размеры внутри колонок — жидкие, через clamp().
Отступы между блоками
Отступы между секциями — ещё одна головная боль. Обычно их задают через фиксированные значения и правят под каждый брейкпоинт. С clamp() это уходит.
.section {
padding-block: var(--space-xl);
}
.section--compact {
padding-block: var(--space-lg);
}
.grid--cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
gap: var(--space-md);
}
minmax(min(100%, 280px), 1fr) — важная деталь. Без min(100%, 280px) на очень узких экранах колонка может оказаться шире контейнера и вызвать горизонтальный скролл. Внутренний min() гарантирует, что минимальная ширина трека не превысит доступную ширину родителя.
Часть 2. Container Queries — компонент сам знает, где он живёт
Медиа-запросы реагируют на вьюпорт. Контейнерные запросы реагируют на ширину родительского блока. Разница принципиальная: одна и та же карточка товара может лежать в широкой сетке каталога, в узком слайдере «Вы смотрели» и в модальном окне. С медиа-запросами придётся писать три набора стилей. С контейнерными — один.
Как объявить контейнер
Контейнерный запрос работает только если у родителя явно задан container-type. Обычно достаточно inline-size — запросы по ширине.
.catalog--grid {
container-type: inline-size;
container-name: catalog-grid;
}
Имя контейнера не обязательно, но полезно, когда контейнеры вложены друг в друга и нужно точно указать, к какому именно обращаться. Без имени запрос будет искать ближайший контейнер с подходящим container-type.
Карточка товара в catalog.section
Самое частое место, где ломается верстка при переносе — это карточка товара. В сетке она горизонтальная, в слайдере — вертикальная, в сайдбаре — компактная. Раньше для этого писались классы-модификаторы и JS переключал их по ResizeObserver. Теперь достаточно контейнерного запроса.
.product-card {
display: flex;
flex-direction: column;
gap: var(--space-sm);
padding: var(--space-sm);
border: 1px solid var(--color-border);
border-radius: 8px;
}
@container catalog-grid (min-width: 400px) {
.product-card {
flex-direction: row;
align-items: center;
}
.product-card--image {
flex: 0 0 120px;
}
.product-card--body {
flex: 1;
min-width: 0;
}
}
@container catalog-grid (min-width: 700px) {
.product-card--image {
flex-basis: 180px;
}
.product-card--title {
font-size: var(--font-size-lg);
}
}
В шаблоне catalog.section это выглядит так:
<div class="catalog--grid">
<?foreach ($arResult['ITEMS'] as $arItem):?>
<div class="product-card">
<div class="product-card--image">
<img src="<?=$arItem['PREVIEW_PICTURE']['SRC']?>" alt="">
</div>
<div class="product-card--body">
<div class="product-card--title"><?=$arItem['NAME']?></div>
<div class="product-card--price"><?=$arItem['PRICE']?> ₽</div>
<button class="product-card--buy">В корзину</button>
</div>
</div>
<?endforeach;?>
</div>
Карточка не знает, где она находится. Она знает только ширину родителя. Если родитель узкий — вертикальная раскладка. Если широкий — горизонтальная. Перенесли карточку в слайдер — она автоматически перестроилась.
Слайдер товаров
Слайдер — классический пример, где контейнерные запросы экономят часы работы. Раньше приходилось держать отдельный шаблон для слайдера с урезанной версткой карточки. Теперь можно использовать один шаблон и один набор стилей.
.slider--track {
container-type: inline-size;
container-name: product-slider;
display: flex;
gap: var(--space-sm);
overflow-x: auto;
scroll-snap-type: x mandatory;
}
.slider--item {
flex: 0 0 clamp(200px, 60vw, 280px);
scroll-snap-align: start;
}
@container product-slider (max-width: 500px) {
.product-card {
flex-direction: column;
}
.product-card--image {
flex: none;
width: 100%;
aspect-ratio: 1;
object-fit: contain;
}
}
В слайдере карточка всегда вертикальная, потому что контейнер узкий. В сетке каталога — горизонтальная, потому что контейнер широкий. Один компонент, разные контексты.
Фильтр в каталоге
Фильтр — ещё один компонент, который страдает от контекста. В сайдбаре он вертикальный, в мобильной версии — выезжающая панель, в горизонтальной раскладке — строка чекбоксов.
.filter--wrapper {
container-type: inline-size;
container-name: filter-panel;
}
.filter--group {
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
@container filter-panel (min-width: 600px) {
.filter--group {
flex-direction: row;
flex-wrap: wrap;
align-items: center;
gap: var(--space-sm);
}
.filter--label {
white-space: nowrap;
}
}
@container filter-panel (max-width: 400px) {
.filter--group {
padding: var(--space-xs);
}
.filter--label {
font-size: var(--font-size-xs);
}
}
Обратите внимание: container-type задан на .filter--wrapper, а не на самой группе. Контейнерный запрос не может применяться к элементу, который сам является контейнером — нужен родитель.
Часть 3. Интеграция в Битрикс-проект
Где держать глобальные переменные
Все clamp()-переменные и базовые токены лучше объявлять в template_styles.css шаблона сайта. Этот файл подключается на всех страницах, и переменные доступны любому компоненту. Если какой-то компонент переиспользуется в нескольких проектах, его локальные переменные можно продублировать в style.css самого компонента — это нормально, потому что компонент должен быть самодостаточным.
/* template_styles.css */
:root {
--font-size-base: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
--space-md: clamp(1rem, 0.8rem + 1vw, 1.5rem);
--color-border: #e0e0e0;
}
Чистый template.php без JS-костылей
Раньше, чтобы переключить карточку из горизонтальной в вертикальную при переносе в слайдер, приходилось либо дублировать шаблон, либо писать JS, который через ResizeObserver измерял ширину родителя и вешал класс .is-narrow. Теперь этого не нужно.
В template.php остаётся только разметка. Никаких проверок ширины, никаких условных классов.
<div class="catalog--grid">
<?foreach ($arResult['ITEMS'] as $arItem):?>
<div class="product-card">
...
</div>
<?endforeach;?>
</div>
Стили компонента — в style.css, который Bitrix подключает автоматически, если он есть в папке шаблона компонента. Никаких инлайновых стилей, никаких подключений через Asset::addCss() — только штатный механизм.
Поддержка браузеров и полифилы
Container Queries поддерживаются во всех актуальных браузерах с 2023 года: Chrome 105+, Firefox 110+, Safari 16+. По состоянию на 2026 год это более 95% глобального трафика. Для коммерческих проектов на Bitrix этого достаточно.
Если проект должен работать в устаревших браузерах, есть два подхода. Первый — progressive enhancement: базовые стили пишутся для медиа-запросов, а контейнерные запросы добавляются сверху. Второй — использовать полифил, но для Container Queries полифилы тяжёлые и не рекомендуются к продакшену. Проще принять, что IE и старые мобильные браузеры получат фиксированную верстку, а современные — адаптивную.
clamp() поддерживается ещё шире: Chrome 79+, Firefox 75+, Safari 13.1+. Здесь проблем с поддержкой нет вообще.
Чек-лист внедрения
- Объявить глобальные CSS-переменные для
clamp()вtemplate_styles.cssшаблона сайта. - Перевести типографику и отступы на жидкие значения — сначала в шаблоне, потом в компонентах.
- Для каждого переиспользуемого компонента (карточка, фильтр, слайдер) задать
container-type: inline-sizeна родительской обёртке. - Написать стили компонента через
@containerвместо@mediaтам, где поведение зависит от родителя. - Медиа-запросы оставить только для структурных изменений: скрыть сайдбар, переключить меню, изменить порядок блоков.
- Проверить верстку во всех контекстах, где компонент используется: сетка, слайдер, сайдбар, модальное окно.
Медиа-запросы не исчезнут полностью. Но их станет на порядок меньше. А компоненты перестанут ломаться при переносе из одного места в другое — и это главное, что даёт переход на clamp() и Container Queries.
Комментарии