RESPONSIVE — правило адаптива
Версия: 2026-06-29.
9 минутВерсия: 2026-06-29.
Одно правило, по которому адаптив пишется одинаково и хорошо во всех наших фронтах. Это предписание, не обзор возможностей CSS. На каждую задачу — один инструмент; развилок «можно так или так» здесь нет. Спутник DESIGN_RULES.md: там — что строить, здесь — как сделать это текучим и переносимым на нашем стеке (BMFP, React, Sass + CSS-переменные).
Правило
Компонент подстраивается под свой контейнер, не под экран.
- Значение (размер, отступ, радиус, шрифт) — текучее, из токена.
clamp, без query. - Структура (стек ↔ строка, число колонок, скрыть) — переключается по ширине контейнера.
@container. @media— только под физику устройства и каркас страницы, из закрытого списка §4.- Внутри компонента про viewport не знают.
Почему так, а не брейкпоинтами по ширине экрана: брейкпоинт привязан к диагонали устройства, а компонент живёт в колонке, слоте, оболочке — их ширина к экрану отношения не имеет. Один и тот же виджет стоит в shell хаба, в MFE, в мини-аппе, в переносимом пакете — везде место разное. Экран про реальную ширину компонента врёт, контейнер — нет. Привязка к контейнеру — единственная, которая не ломается при переносе. Текучесть из токенов убирает дублирование значений. Всё остальное в этом файле — следствие этих двух фактов.
1. Порядок. Всегда сверху вниз
Это не выбор ветки по вкусу — это последовательность. Спускаешься на следующий уровень, только если предыдущий физически не решает задачу.
1. Значение меняется плавно (размер / отступ / радиус / шрифт) -> clamp из токена. Query нет.
2. Меняется структура (стек<->строка / число колонок / скрыть) -> @container, по контейнеру.
3. Это физика устройства (палец, мышь, motion, высота, вырез) -> @media / env, закрытый список §4.
4. Это каркас целой страницы (header, sidebar) -> @media на layout.Пишешь внутри компонента @media по ширине — ты на неправильном уровне. Вернись на 1 или 2.
2. Значение — текучее, из токена
Значение живёт один раз — как CSS-переменная в :root. Компонент его не вычисляет, а берёт.
:root {
--t-body: 1.02rem;
--t-h1: clamp(2rem, 4.2vw, 3rem);
--t-display: clamp(2.5rem, 6vw, 4.1rem);
--s5: 24px;
--r: 12px;
--maxw: 1120px;
}
.title { font-size: var(--t-h1); }Сетка считает число колонок сама, без брейкпоинтов «1 → 2 → 3»:
.grid {
display: grid;
gap: var(--s5);
grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
}Обёртка-контейнер — без лишних слоёв:
.wrap { width: min(100%, var(--maxw)); margin-inline: auto; }Текучесть по контейнеру (для переносимых компонентов) — единицы cqi (1% inline-size контейнера). Шрифт масштабируется от блока, а не от экрана:
.card { font-size: clamp(0.95rem, 4cqi, 1.25rem); }Запрещено: число вместо токена в компоненте; брейкпоинт ради смены размера шрифта или отступа.
3. Структуру переключает контейнер
Контейнер-контекст объявляет родитель — лэйаут или регион:
.region { container: content / inline-size; }Компонент внутри реагирует на ширину этого контейнера:
.card {
display: grid;
gap: var(--s4);
@container content (min-width: 34rem) {
grid-template-columns: auto 1fr;
}
}- Ось запроса —
inline-size, неsize(sizeтребует фиксированной высоты и ломает блоки, растущие по контенту). - Контейнер именовать — иначе запрос цепляется к ближайшему любому предку.
- Запрос смотрит на предка: контекст ставится на обёртку, адаптируется потомок. Сам себя контейнер не запрашивает.
Сменить раскладку компонента можно только так. Viewport для этого не используется никогда.
4. Физика устройства и каркас
@media разрешён только для перечисленного. Чего нет в списке — не повод для @media.
@media (hover: hover) and (pointer: fine)— hover-эффекты только под мышь (DESIGN_RULES.md§7).@media (pointer: coarse)— крупный таргет под палец (§15).@media (prefers-reduced-motion: reduce)— снять/упростить анимацию (§15).@media (prefers-color-scheme: dark)— системная тема, если её не переключаем сменой токенов.@media (min-width: …)на лэйауте — каркас страницы: схлопнуть header, убрать sidebar.
Единицы экрана (это не @media, но та же физика):
- Высота под экран —
dvh/svh, неvh(учитывают адресную строку). - Края под вырез и таб-бар —
env(safe-area-inset-*)+viewport-fit=cover(мобайл, TG-mini-app).
Ширина для раскладки компонента в этом списке отсутствует намеренно — она в §3.
5. Единицы — одна на роль
Одна единица на роль, альтернативы нет.
- Шрифт, отступы, радиусы →
rem, через токен.pxдля шрифта запрещён — ломает zoom и reflow (§15). - Хайрлайны, бордеры, жёсткие константы →
px. - Колонки и доли →
fr,%,minmax,auto-fit/fill. - Текучесть по контейнеру →
cqi/cqb. - Высота под экран →
dvh/svh. - Оси, отступы, скругления → логические свойства:
inline-size,margin-inline,padding-block,inset-inline,border-start-start-radius. Физическиеleft/right/top/bottomзапрещены.
6. Размещение по слоям BMFP
| Слой | Ответственность за адаптив |
|---|---|
boundary/__styles/ | единственный источник: токены (текучие шкалы) + миксины. Другой абстракции нет. |
boundary/layouts/ | объявляют контейнер-контексты (container-type) и каркасные @media страницы. |
boundary/components, widgets/ | каждый *.module.scss реагирует на свой контейнер через @container, значения берёт из токенов. Viewport-@media внутри нет. |
Компонент, привязанный к контейнеру, корректен в любой оболочке — это и есть условие переносимости MFE и пакетов.
7. Канон SCSS — один на все фронты
Один источник. Разрозненные respond() / breakpoint() по проектам — удалить.
boundary/__styles/_tokens.scss — текучие шкалы как CSS-переменные. Имена семантические, не red-500 (DESIGN_RULES.md §16).
boundary/__styles/_responsive.scss — четыре миксина, не больше:
@mixin container($name) { container: #{$name} / inline-size; }
@mixin cq($name, $min) { @container #{$name} (min-width: #{$min}) { @content; } }
@mixin coarse { @media (pointer: coarse) { @content; } }
@mixin reduced { @media (prefers-reduced-motion: reduce) { @content; } }компонент адаптируется -> @include cq(...) (контейнер)
страница / shell -> @media (вьюпорт)8. Запрещено
- Брейкпоинт-оверрайд, переобъявляющий
font-size/padding, которые уже заданы черезclamp. @mediaпо ширине внутри компонента.100vhна полноэкранных блоках — прыгает под адресной строкой. Толькоdvh/svh.- Сетка по брейкпоинтам «1 → 2 → 3» вместо
auto-fit minmax. - Магические px-брейки (520, 768) врассыпную без системы.
- Hover без
(hover: hover)— на тач-устройстве залипает. - Фиксированная высота под объём контента — текст обрезается или переполняет.
- Отключённый zoom (
user-scalable=no,maximum-scale=1). - Шрифт в
px. - Физические
left/rightв новом коде.
9. Проверка
Через агент-поверхность приложения и ресайз контейнера, не глаз по скриншоту. Дёргаешь ширину контейнера компонента в обёртке — видишь все его состояния изолированно, без прогона всего вьюпорта.
Обязательный прогон:
- reflow до 320px по ширине без горизонтального скролла (§15);
- zoom 200% — текст не обрезан, ничего не наезжает;
prefers-reduced-motion— анимации сняты;pointer: coarse— таргеты не мельче нормы;- состояния из
DESIGN_RULES.md§7 — на узком и широком контейнере, не только на «десктопе».
10. Новый компонент — по шагам
- Сверстать в один поток; значения тянуть из токенов (
var(--t-*),var(--s*),var(--r*)). Чисел врассыпную нет. - Закрыть текучесть
clamp/min/max; сетку —auto-fit minmax. На этом шаге, как правило, ноль query. - Нужна смена структуры — обернуть регион в
@include container(name), добавить@include cq(name, 34rem) { … }. - Hover, таргеты, мошн — миксинами
@mediaиз §7. - Полноэкранное —
dvh/svh; края под вырез —env(safe-area-inset-*). - Проверить ресайзом контейнера + reflow 320 / zoom 200, не скриншотом.
11. Перенос desktop → mobile: по смыслу, не по ширине
Адаптив — переосмысление под контекст, не сжатие десктопа. Узкий экран — другое устройство и другой способ смотреть, а не уменьшенный монитор. Машинально стекать всё в вертикаль и тащить десктоп-эффекты вниз — это не адаптив.
Горизонтальный ряд по умолчанию НЕ ломать в вертикальный стек. Если на десктопе блок — это ряд однотипного (галерея, карточки, сравнение), на мобайле чаще правильнее оставить его рядом и дать горизонтальную прокрутку (overflow-x: auto + scroll-snap), а не разворачивать в длинный вертикальный список — так сохраняется смысл «это один ряд».
- Презентационный ряд (галерея, логотипы, скриншоты) — авто-прокрутка сама + пользователь может листать рукой (свайп перехватывает авто и отдаёт управление).
scroll-snap-type: inline mandatory. - Сравнительный ряд (тарифы, «что выбрать», сравнение) — НЕ авто: листает сам пользователь, элементы остаются в строку бок о бок. Сравнение в голове складывается легче, когда варианты рядом и видны вместе, а не разнесены вертикально.
- Всегда — видимый сигнал, что есть продолжение (обрезанный край следующего элемента, точки-индикатор), иначе дальше первого не посмотрят.
Объёмный функционал сворачивать и приоритизировать. То, что на десктопе развёрнуто целиком (большое меню, панель фильтров, набор действий, таблица, сайдбар), на мобайле НЕ оставлять как есть — сгруппировать по важности и убрать второстепенное за раскрытие: бургер-меню, выпадающие списки, аккордеоны, «ещё», bottom-sheet, табы. На экране сразу — только главное, остальное в один тап (прогрессивное раскрытие). Мобайл не вмещает десктопный объём, поэтому решают приоритетом и сворачиванием, а не уплотнением всего в один экран.
Десктопные «фишки» не переносить механически. Эффект, дающий изюминку на большом экране (сложный hover, параллакс, крупная футуристичная графика, текст-за-объектом), на телефоне часто не читается или мешает — и не выглядит так же круто. Не масштабировать его вниз, а придумать мобильную замену: свой приём под маленький тач-экран. Где десктоп-эффект не переносится — на мобайле его нет, а на его месте своё. Это и есть адаптация к ситуации, а не к ширине.
Откройте исходник документа по ссылке «Предложить правку» — там же видно, что и когда в нём менялось.