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. Порядок. Всегда сверху вниз

Это не выбор ветки по вкусу — это последовательность. Спускаешься на следующий уровень, только если предыдущий физически не решает задачу.

text
1. Значение меняется плавно (размер / отступ / радиус / шрифт)   -> clamp из токена. Query нет.
2. Меняется структура (стек<->строка / число колонок / скрыть)   -> @container, по контейнеру.
3. Это физика устройства (палец, мышь, motion, высота, вырез)    -> @media / env, закрытый список §4.
4. Это каркас целой страницы (header, sidebar)                   -> @media на layout.

Пишешь внутри компонента @media по ширине — ты на неправильном уровне. Вернись на 1 или 2.


2. Значение — текучее, из токена

Значение живёт один раз — как CSS-переменная в :root. Компонент его не вычисляет, а берёт.

scss
: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»:

scss
.grid {
  display: grid;
  gap: var(--s5);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
}

Обёртка-контейнер — без лишних слоёв:

scss
.wrap { width: min(100%, var(--maxw)); margin-inline: auto; }

Текучесть по контейнеру (для переносимых компонентов) — единицы cqi (1% inline-size контейнера). Шрифт масштабируется от блока, а не от экрана:

scss
.card { font-size: clamp(0.95rem, 4cqi, 1.25rem); }

Запрещено: число вместо токена в компоненте; брейкпоинт ради смены размера шрифта или отступа.


3. Структуру переключает контейнер

Контейнер-контекст объявляет родитель — лэйаут или регион:

scss
.region { container: content / inline-size; }

Компонент внутри реагирует на ширину этого контейнера:

scss
.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 — четыре миксина, не больше:

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; } }
text
компонент адаптируется -> @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. Новый компонент — по шагам

  1. Сверстать в один поток; значения тянуть из токенов (var(--t-*), var(--s*), var(--r*)). Чисел врассыпную нет.
  2. Закрыть текучесть clamp / min / max; сетку — auto-fit minmax. На этом шаге, как правило, ноль query.
  3. Нужна смена структуры — обернуть регион в @include container(name), добавить @include cq(name, 34rem) { … }.
  4. Hover, таргеты, мошн — миксинами @media из §7.
  5. Полноэкранное — dvh / svh; края под вырез — env(safe-area-inset-*).
  6. Проверить ресайзом контейнера + reflow 320 / zoom 200, не скриншотом.

11. Перенос desktop → mobile: по смыслу, не по ширине

Адаптив — переосмысление под контекст, не сжатие десктопа. Узкий экран — другое устройство и другой способ смотреть, а не уменьшенный монитор. Машинально стекать всё в вертикаль и тащить десктоп-эффекты вниз — это не адаптив.

Горизонтальный ряд по умолчанию НЕ ломать в вертикальный стек. Если на десктопе блок — это ряд однотипного (галерея, карточки, сравнение), на мобайле чаще правильнее оставить его рядом и дать горизонтальную прокрутку (overflow-x: auto + scroll-snap), а не разворачивать в длинный вертикальный список — так сохраняется смысл «это один ряд».

  • Презентационный ряд (галерея, логотипы, скриншоты) — авто-прокрутка сама + пользователь может листать рукой (свайп перехватывает авто и отдаёт управление). scroll-snap-type: inline mandatory.
  • Сравнительный ряд (тарифы, «что выбрать», сравнение) — НЕ авто: листает сам пользователь, элементы остаются в строку бок о бок. Сравнение в голове складывается легче, когда варианты рядом и видны вместе, а не разнесены вертикально.
  • Всегда — видимый сигнал, что есть продолжение (обрезанный край следующего элемента, точки-индикатор), иначе дальше первого не посмотрят.

Объёмный функционал сворачивать и приоритизировать. То, что на десктопе развёрнуто целиком (большое меню, панель фильтров, набор действий, таблица, сайдбар), на мобайле НЕ оставлять как есть — сгруппировать по важности и убрать второстепенное за раскрытие: бургер-меню, выпадающие списки, аккордеоны, «ещё», bottom-sheet, табы. На экране сразу — только главное, остальное в один тап (прогрессивное раскрытие). Мобайл не вмещает десктопный объём, поэтому решают приоритетом и сворачиванием, а не уплотнением всего в один экран.

Десктопные «фишки» не переносить механически. Эффект, дающий изюминку на большом экране (сложный hover, параллакс, крупная футуристичная графика, текст-за-объектом), на телефоне часто не читается или мешает — и не выглядит так же круто. Не масштабировать его вниз, а придумать мобильную замену: свой приём под маленький тач-экран. Где десктоп-эффект не переносится — на мобайле его нет, а на его месте своё. Это и есть адаптация к ситуации, а не к ширине.

Инструкция не помогла?

Откройте исходник документа по ссылке «Предложить правку» — там же видно, что и когда в нём менялось.