Гайд по написанию адаптивных компонентов
December 29, 2025 · View on GitHub
Есть два способа:
- CSS Media Queries
- JS Media Queries
Для поддержки SSR приоритет мы отдаём первому способу.
CSS Media Queries
Изначально писать адаптивность стоит через CSS Media Queries.
Для медиа запросов мы используем custom-media-queries. Текущие медиа запросы можно посмотреть в типе CSSCustomMedias.
При разработке следует предусмотреть возможность переопределения параметров адаптивности через AdaptivityProvider.
Пример 1.
<AdaptivityProvider density="compact">
<Component>lorem ipsum</Component>
</AdaptivityProvider>
Component.tsx
import { classNames } from '@vkontakte/vkjs';
import { useAdaptivity } from '../../hooks/useAdaptivity';
import styles from './Component.module.css';
const densityClassNames = {
none: styles.hostdensityNone, // означает, что `density` не определён в `AdaptivityProvider` – используем `@media`
compact: styles.hostdensityCompact,
};
const Component = () => {
const { density = 'none' } = useAdaptivity();
return (
<div
className={classNames(
styles.host,
// компонент слушает только compact
density !== 'regular' && densityClassNames[density],
)}
/>
);
};
Component.module.css
/* Равносильно модификатору `densityRegular` */
.host {
color: red;
padding: 20px;
}
.densityCompact {
padding: 10px;
}
@media (--density-compact) {
.densityNone {
padding: 10px;
}
}
Мы задаём padding: 10px; для размера density-compact и padding: 20px; для размера density-regular.
В @media (--density-compact) мы задаём padding: 10px; для компонента только если density не переопределен.
Пример 2.
<AdaptivityProvider viewWidth={ViewWidth.TABLET}>
<Component>lorem ipsum</Component>
</AdaptivityProvider>
Component.tsx
import { classNames } from '@vkontakte/vkjs';
import { useAdaptivity } from '../../hooks/useAdaptivity';
import { ViewWidth, viewWidthToClassName } from '../../../lib/adaptivity';
import styles from './Component.module.css';
const viewWidthClassNames = {
none: styles.viewWidthNone, // означает, что viewWidth не определён в AdaptivityProvider – используем `@media`
smallTabletMinus: styles.viewWidthSmallTabletMinus,
smallTabletPlus: styles.viewWidthSmallTabletPlus,
};
const Component = () => {
const { viewWidth } = useAdaptivity();
return (
<div
className={classNames(styles.Component, viewWidthToClassName(viewWidthClassNames, viewWidth))}
/>
);
};
Component.module.css
.host {
color: red;
}
.viewWidthSmallTabletPlus {
color: blue;
}
@media (--viewWidth-smallTabletPlus) {
.viewWidthNone {
color: blue;
}
}
.viewWidthSmallTabletMinus {
color: green;
}
@media (--viewWidth-smallTabletMinus) {
.viewWidthNone {
color: green;
}
}
По историческим причинам, в JS (см. AdaptivityProvider) и в CSS (см. customMedias.generated.css) по-разному определяются брейкпоинты. В CSS они выходят более расширенными, а в JS ограничиваются точечными значениями. Для конвертации этой разницы создана функция viewWidthToClassName.
Хук useAdaptivityConditionalRender
Помогает скрывать/показать блок в зависимости от @media или AdaptivityProvider.
Подводные камни
В процессе перевода существующих компонентов на новую систему адаптивности возникли места, в которых пришлось отступить от стандартного поведения.
-
.modeNoneВ компоненте Group появился класс
.modeNone. Он означает, что уGroupне переданmodeи не удалось вычислить его автоматически..modeNoneдолжен вести себя как.modeCardприviewWidth={ViewWidth.SMALL_TABLET}и как.modePlainприviewWidth={ViewWidth.MOBILE}. Пример использования:.modeCard .сomponent { padding-inline: 8px; } @media (--viewWidth-smallTabletPlus) { /* Применяем стили `.modeCard`, если не задан `mode` и `viewWidth={ViewWidth.SMALL_TABLET}` */ .modeNone .in { padding-inline: 8px; } }
JS Media Queries
JS Media Queries мы используем только для компонентов, которые гарантировано будут показаны после первого рендера страницы. Иначе пользователи, которые используют SSR, получат ошибку при гидратации. На стороне сервера нет доступа к клиентским API, которые используются для реализации JS Media Queries.
Зачастую это всплывающие окна: модалки, тултипы, дропдауны и др.
Почему мы не рекомендуем использовать CSS Media Queries для всплывающих окон
В случаях, где внешний вид компонента меняется в зависимости от размера вьюпорта, для адаптивности через CSS мы должны
создать несколько разметок (для мобильного и десктопа, например) и показывать только одну из них. Для всплывающих окон
такой подход обозначает дублирование атрибута id. При этом с точки зрения UX такие окна появляются либо после вызова
их программно, либо после вызова их пользователем, то есть рендериться на сервере они не будут. Поэтому для всплывающих
окон мы используем адаптивность через JS. Для этого создан хук useAdaptivityWithJSMediaQueries().
При этом мы помним про показ компонента только после первого рендера. Пример:
// ❌ bad for SSR
const App = () => {
return (
<ModalRoot activeModal="main">
<ModalPage id="main">Hello World!</ModalPage>
</ModalRoot>
);
};
// ✅ good for SSR
const App = () => {
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
setMounted(true);
}, []);
return mounted ? (
<ModalRoot activeModal="main">
<ModalPage id="main">Hello World!</ModalPage>
</ModalRoot>
) : null;
};
В нашем примере внутри <ModalRoot /> и <ModalPage /> используется useAdaptivityWithJSMediaQueries() для
переключения компонентов на мобильный или десктопный виды.
В библиотеке логику с состоянием mounted инкапсулирует в себе компонент AppRootPortal.