Реактивное булево состояние на основе CSS media queries и размеров элементов для Vanilla JS, Vue 3, Nuxt и React 19+ — без обязательных peer-зависимостей.

CSS media query решает, как выглядит вёрстка, но ничего не сообщает JavaScript — responsive-media превращает тот же самый media query в обычное реактивное булево значение, которое можно читать прямо в логике компонента, без отдельного resize-listener и ручного дебаунса.
На мобильном показывается пять карточек в ленте, на десктопе — таблица на двадцать колонок. Это не вопрос стилей, а разный набор данных и разная логика рендера — компонент читает текущий брейкпоинт как обычное реактивное значение и сам решает, что рендерить.
Один и тот же дашборд-виджет в узкой боковой панели должен схлопываться в компактный вид, а в широкой центральной колонке — раскрываться полностью. Виджет отслеживает размер собственного контейнера, а не всего экрана, и переключается сам.
Системная настройка «уменьшить движение» должна реально отключать переходы и параллакс на сайте — она доступна как то же самое реактивное значение, что и обычные брейкпоинты, и на неё можно так же просто подписаться.
На сервере ещё не известен реальный размер экрана пользователя — состояние безопасно переносится с сервера на клиент без рассинхронизации, и вёрстка не дёргается, переключаясь с одной версии на другую сразу после загрузки.

Отслеживайте изменения вьюпорта и размеров элементов через единый API. ReactiveResponsiveState использует window.matchMedia для брейкпоинтов, а ContainerState — ResizeObserver для оценки размеров и ориентации элемента. Состояние всегда актуально и обновляется без лишних ререндеров.

Описывайте брейкпоинты как массив условий (AND) или вложенный массив групп (OR). Поддерживаются min/max-width/height, orientation, aspect-ratio, prefers-color-scheme, hover, pointer, resolution, display-mode и raw-строки. Это даёт полный контроль над логикой адаптивности.

Подписывайтесь на общие изменения (subscribe), конкретные ключи (on), переходы (onEnter/onLeave), однократные события (once) и ожидайте выполнения условия (waitFor). Упорядоченные брейкпоинты дают методы current, isAbove, isBelow и between для семантических сравнений.

Синхронизируйте состояние с CSS-переменными (syncCSSVars), генерируйте DOM-события (emitDOMEvents), преобразуйте ключи в сигналы (toSignal) и выбирайте значения по первому активному брейкпоинту (match). Используйте готовые пресеты для Tailwind, Bootstrap и доступности.

Vue-композаблы (useResponsive, useBreakpoints, useMediaQuery, useContainerState) и React-хуки на useSyncExternalStore обеспечивают реактивность в шаблонах и компонентах. Модуль Nuxt берёт брейкпоинты из nuxt.config.ts, генерирует типы по ним, гидратируется без предупреждений о несовпадении и умеет рендерить сервер под устройство посетителя; defineResponsive выводит ключи состояния из вашего конфига. Все API SSR-безопасны, поддерживают гидратацию (в том числе ssrState для серверной раскладки) и не требуют обязательных peer-зависимостей.

Ядро библиотеки не зависит ни от одного фреймворка — Vue- и React-интеграции лишь тонко оборачивают один и тот же runtime, так что логику можно переиспользовать и в vanilla-коде. createResponsiveState() создаёт изолированные экземпляры состояния, а hydrate() безопасно переносит серверное состояние на клиент.
responsiveState уже настроен под mobile/tablet/desktop из коробки — getState() отдаёт стабильный снапшот, proxy — живой доступ без дебаунса, а getResponsiveMediaQueries() возвращает те же условия готовыми CSS-строками.
import { responsiveState, setResponsiveConfig, getResponsiveMediaQueries } from 'responsive-media'
// Re-configure the singleton
setResponsiveConfig(
{
sm: [{ type: 'max-width', value: 767 }],
lg: [{ type: 'min-width', value: 1024 }],
},
{
order: ['sm', 'lg'], // for isAbove / isBelow / between
debounce: 50, // ms — throttle subscribe() listeners
},
)
// Read a stable snapshot
const { sm, lg } = responsiveState.getState()
// Live proxy access (never debounced)
console.log(responsiveState.proxy.sm)
// Get the generated CSS strings
const mq = getResponsiveMediaQueries()
// { sm: '(max-width: 767px)', lg: '(min-width: 1024px)' }createContainerState следит за размером конкретного DOM-элемента через ResizeObserver и сам переключает классы/CSS-переменные — та же идея, что CSS Container Queries, но с поддержкой браузеров без них.
import { createContainerState } from 'responsive-media/container'
// or: import { createContainerState } from 'responsive-media';
const card = document.querySelector('.card')!
const cardState = createContainerState(
card,
{
compact: [{ type: 'max-width', value: 300 }],
normal: [
{ type: 'min-width', value: 301 },
{ type: 'max-width', value: 599 },
],
wide: [{ type: 'min-width', value: 600 }],
},
{
order: ['compact', 'normal', 'wide'],
},
)
// Reactive class toggling
cardState.on('compact', (v) => card.classList.toggle('card--compact', v))
// Sync CSS custom properties: --card-compact: 1; --card-wide: 0; …
cardState.syncCSSVars({ prefix: '--card-' })
// Get @container-compatible query strings
const strings = cardState.getMediaQueries()
// { compact: '(max-width: 300px)', wide: '(min-width: 600px)' }
// Cleanup — call once you're done watching this element (e.g. before
// removing it from the DOM), not right after setup
// cardState.destroy()