responsive-media — реактивные брейкпоинты для Vue и не только

В веб-приложениях то и дело нужно знать «мы на мобилке или на десктопе»: менять вёрстку, показывать другой набор кнопок, подставлять разные размеры картинок или просто логировать тип устройства. Писать везде window.matchMedia('(max-width: 768px)') и вручную подписываться на change — неудобно и легко размазать логику по компонентам. Пакет responsive-media как раз про то, чтобы один раз описать брейкпоинты, получить реактивное состояние (мобилка / планшет / десктоп или свои варианты) и использовать его и в «голом» JS, и во Vue 3 без лишней возни.
В этом посте — зачем выносить медиа-запросы в отдельный слой, как устроен конфиг брейкпоинтов (в том числе с несколькими условиями в одном), как пользоваться ядром без фреймворка и как подключить плагин для Vue 3. Цель — дать полную картину возможностей с примерами под типичные задачи: от простого «мобилка/десктоп» до кастомных брейкпоинтов с ориентацией и aspect-ratio.
Зачем вообще реактивное состояние по медиа-запросам
На практике с адаптивом часто получается так: в одном компоненте проверяем ширину для условного рендера, в другом — для стилей или запроса к API, в третьем — снова та же проверка, но с другими числами. Типичные ситуации:
- Единый источник правды: хочется один раз определить «что такое мобилка, планшет, десктоп» и во всём приложении опираться на одни и те же флаги. Иначе в одном месте мобилка — это
max-width: 600, в другом —768, и поведение расходится. - Реактивность без костылей: при изменении размера окна (или ориентации устройства) интерфейс должен обновляться сам. Ручная подписка на
matchMedia().addEventListener('change')в каждом компоненте ведёт к дублированию и риску забыть отписаться. - Гибкие условия: не только ширина, но и ориентация, aspect-ratio, разрешение экрана — например, «мобилка в портрете» или «широкий экран с соотношением 16:9». Писать и комбинировать такие запросы вручную неудобно.
responsive-media даёт единый слой: вы задаёте набор брейкпоинтов (по умолчанию mobile / tablet / desktop), при необходимости добавляете свои и комбинируете несколько условий в одном. Состояние обновляется при изменении медиа-запросов, подписка через subscribe или во Vue через useResponsive() — и логика «где мы сейчас по ширине/ориентации» живёт в одном месте. В итоге и дизайн-систему держать проще, и тесты писать удобнее: можно подменять конфиг или состояние без размазывания проверок по компонентам.
Что внутри: ядро и плагин для Vue 3
Пакет небольшой: ядро с реактивным состоянием и подпиской, плюс плагин для Vue 3, который прокидывает то же состояние в приложение через provide/inject и композицию. Подключаете только то, чем пользуетесь.
| Что подключать | Назначение |
|---|---|
| responsive-media (ядро) | responsiveState, setResponsiveConfig, getResponsiveMediaQueries. Работает в любом окружении (браузер с window.matchMedia). |
| Vue 3 | ResponsivePlugin и useResponsive() — реактивное состояние в компонентах без ручной подписки. |
Установка — одна команда:
bash
npm install responsive-media
Для Vue-проекта нужна peer-зависимость vue (^3.5.27) — как правило, она уже есть. Типы поставляются из коробки (TypeScript).
Как устроен конфиг брейкпоинтов
Всё строится вокруг объекта конфигурации: ключ — имя брейкпоинта (например mobile, tablet, desktop), значение — массив условий. Каждое условие — объект с полями type и value. Все условия одного брейкпоинта объединяются через and в один медиа-запрос.
Пример: «мобилка» = ширина не больше 600px, «планшет» = не больше 960px, «десктоп» = от 961px.
| Поле | Описание |
|---|---|
| type | Тип медиа-условия: min-width, max-width, width, min-height, max-height, aspect-ratio, orientation, resolution и др. (все стандартные CSS media features). |
| value | Число (для ширины/высоты подставится px) или строка (например '16/9' для aspect-ratio, 'portrait' для orientation). |
Дефолтный конфиг выглядит так:
ts
import { ResponsiveConfig } from 'responsive-media';
// ResponsiveConfig по умолчанию:
{
mobile: [{ type: 'max-width', value: 600 }],
tablet: [{ type: 'max-width', value: 960 }],
desktop: [{ type: 'min-width', value: 961 }],
}
Важно: условия внутри одного брейкпоинта комбинируются через and. Например, «мобилка в портретной ориентации»:
ts
mobile: [
{ type: 'max-width', value: 500 },
{ type: 'orientation', value: 'portrait' },
]
Получится медиа-запрос (max-width: 500px) and (orientation: portrait).
Использование без Vue: состояние и подписка
Ядро даёт глобальный синглтон responsiveState. Через него вы читаете текущее состояние и подписываетесь на изменения.
Чтение состояния — через responsiveState.proxy: объект с булевыми флагами по каждому брейкпоинту. В какой момент активен ровно один из «ширинных» брейкпоинтов — зависит от конфига (при пересекающихся условиях могут быть несколько true).
ts
import { responsiveState } from 'responsive-media';
const { mobile, tablet, desktop } = responsiveState.proxy;
console.log('isMobile:', mobile);
console.log('isTablet:', tablet);
console.log('isDesktop:', desktop);
Подписка на изменения — метод subscribe. Возвращает функцию отписки. При первом вызове подписчик сразу получает текущее состояние.
ts
const unsubscribe = responsiveState.subscribe((state) => {
console.log('State changed:', state);
// state.mobile, state.tablet, state.desktop
});
// когда не нужен списокенр:
unsubscribe();
Внутри используется window.matchMedia и событие change, так что обновление происходит при изменении размера окна, ориентации или других заданных в конфиге медиа-условий.
Кастомные брейкпоинты и комбинированные условия
Чтобы заменить дефолты или добавить свои брейкпоинты, используется setResponsiveConfig. В конфиг можно передать и дефолт (размазав ResponsiveConfig), и переопределения, и новые ключи.
Пример: свои границы и новый брейкпоинт «широкий экран»
ts
import { setResponsiveConfig, ResponsiveConfig } from 'responsive-media';
setResponsiveConfig({
...ResponsiveConfig,
mobile: [{ type: 'max-width', value: 500 }],
tablet: [
{ type: 'min-width', value: 501 },
{ type: 'max-width', value: 900 },
],
desktop: [{ type: 'min-width', value: 901 }],
wide: [
{ type: 'min-width', value: 1200 },
{ type: 'aspect-ratio', value: '16/9' },
],
});
После этого в responsiveState.proxy появятся поля mobile, tablet, desktop, wide. Поддержка TypeScript для кастомных ключей зависит от того, как типизирован конфиг (дефолтные типы заданы для Breakpoint = 'mobile' | 'tablet' | 'desktop'; при добавлении своих ключей можно расширить тип или использовать индексную сигнатуру).
Пример: «мобилка только в портрете»
Удобно показывать упрощённый UI на узком экране в портрете, а в ландшафте оставить общий вариант:
ts
setResponsiveConfig({
...ResponsiveConfig,
mobile: [
{ type: 'max-width', value: 600 },
{ type: 'orientation', value: 'portrait' },
],
});
Строки медиа-запросов для CSS или отладки
Иногда нужно получить готовую строку медиа-запроса — например, для передачи в утилиту, генерации CSS или логов. Для этого служит getResponsiveMediaQueries(): возвращает объект «имя брейкпоинта → строка запроса».
ts
import { getResponsiveMediaQueries } from 'responsive-media';
const mediaQueries = getResponsiveMediaQueries();
console.log(mediaQueries.mobile); // '(max-width: 600px)'
console.log(mediaQueries.tablet); // '(max-width: 960px)'
console.log(mediaQueries.desktop); // '(min-width: 961px)'
Формат строк совпадает с тем, что принимает window.matchMedia() и обычный CSS @media. Удобно подставлять в стили или в тесты.
Плагин для Vue 3
Во Vue-приложении не хочется в каждом компоненте вызывать responsiveState.subscribe и следить за отпиской. Плагин ResponsivePlugin поднимает состояние в реактивную обёртку и прокидывает его через provide, а композиция useResponsive() возвращает это же состояние в компонентах. Обновление при изменении медиа-запросов происходит автоматически, перерисовка — за счёт реактивности Vue.
Регистрация плагина
С дефолтными брейкпоинтами:
ts
import { createApp } from 'vue';
import { ResponsivePlugin } from 'responsive-media';
import App from './App.vue';
const app = createApp(App);
app.use(ResponsivePlugin);
app.mount('#app');
С кастомным конфигом (можно переопределить дефолты и добавить свои ключи):
ts
import { ResponsivePlugin, ResponsiveConfig } from 'responsive-media';
app.use(ResponsivePlugin, {
...ResponsiveConfig,
mobile: [
{ type: 'max-width', value: 500 },
{ type: 'orientation', value: 'portrait' },
],
desktop: [{ type: 'min-width', value: 961 }],
wide: [
{ type: 'min-width', value: 1200 },
{ type: 'aspect-ratio', value: '16/9' },
],
});
Конфиг, переданный в app.use(ResponsivePlugin, config), применяется через setResponsiveConfig, так что глобальное состояние и то, что видят компоненты, совпадают.
Использование в компоненте
В любом дочернем компоненте подключаете композицию и получаете реактивный объект с флагами брейкпоинтов:
vue
<script setup lang="ts">
import { useResponsive } from 'responsive-media';
const responsive = useResponsive();
</script>
<template>
<div>
<p v-if="responsive.mobile">Мобильная версия</p>
<p v-else-if="responsive.tablet">Планшет</p>
<p v-else>Десктоп</p>
<aside v-if="responsive.desktop">Боковая панель только на десктопе</aside>
</div>
</template>
Состояние обновляется при изменении размера окна или ориентации — перерасчёт и ре-рендер делает Vue. Никакой ручной подписки и отписки в компоненте не нужно.
Композables и вычисляемые значения
Удобно выносить производные значения в computed:
vue
<script setup lang="ts">
import { computed } from 'vue';
import { useResponsive } from 'responsive-media';
const responsive = useResponsive();
const showSidebar = computed(() => responsive.desktop || responsive.tablet);
const isNarrow = computed(() => responsive.mobile);
</script>
<template>
<button v-if="isNarrow">Меню</button>
<nav v-else>...</nav>
</template>
Типы медиа-условий
В конфиге в поле type можно использовать стандартные медиа-фичи. Основные на практике:
| type | value (пример) | Результат |
|---|---|---|
min-width, max-width, width |
число | подставляется px |
min-height, max-height, height |
число | подставляется px |
aspect-ratio, min-aspect-ratio, max-aspect-ratio |
строка, напр. '16/9' |
без px |
orientation |
'portrait' | 'landscape' |
|
resolution, min-resolution, max-resolution |
число или строка (напр. 2 или '2dppx') |
|
color, min-color, max-color |
число | бит на цвет |
monochrome, grid, scan и др. |
по спецификации CSS |
Для aspect-ratio и других нечисловых значений value задаётся строкой. Для ширины/высоты число автоматически превращается в пиксели (например 600 → 600px).
Примеры под реальные задачи
1. Условный рендер и мобильное меню
Задача: на мобилке показывать кнопку «Меню», на десктопе — полноценную навигацию. Один источник правды для «мы на мобилке».
vue
<script setup lang="ts">
import { useResponsive } from 'responsive-media';
const responsive = useResponsive();
</script>
<template>
<header>
<button v-if="responsive.mobile" aria-label="Меню" @click="openMenu">☰</button>
<nav v-else class="desktop-nav">...</nav>
</header>
</template>
Брейкпоинты задаются один раз при старте приложения — во всех компонентах responsive.mobile означает одно и то же.
2. Разные данные или запросы в зависимости от ширины
Задача: на узком экране запрашивать сокращённый список, на широком — полный. Используем состояние в композиции или в запросе.
ts
import { computed } from 'vue';
import { useResponsive } from 'responsive-media';
const responsive = useResponsive();
const listParams = computed(() =>
responsive.mobile ? { limit: 10 } : { limit: 50 }
);
// Дальше listParams подставляется в запрос (например, в watch или в useQuery)
Так логика «что запрашивать» остаётся в одном месте и автоматически реагирует на ресайз.
3. Строки медиа-запросов для стилей или тестов
Задача: использовать те же границы, что и в JS, в CSS или в e2e-тестах. Получаем строки из того же конфига.
ts
import { getResponsiveMediaQueries } from 'responsive-media';
const { mobile, desktop } = getResponsiveMediaQueries();
// В тесте или в утилите: window.matchMedia(mobile).matches
// В CSS-in-JS или при генерации стилей — подставить mobile/desktop как строку
Границы не дублируются: меняете конфиг — обновляются и состояние, и строки запросов.
Краткий итог
responsive-media закрывает типичные задачи при адаптиве: один раз описать брейкпоинты (в том числе с несколькими условиями через and), получать реактивное состояние в ядре через responsiveState.proxy и subscribe, во Vue — через ResponsivePlugin и useResponsive(). Кастомизация через setResponsiveConfig или конфиг плагина, при необходимости — готовые строки медиа-запросов через getResponsiveMediaQueries(). В итоге логика «мобилка / планшет / десктоп» или свои варианты живут в одном месте, код остаётся предсказуемым, а подписки и обновление UI берёт на себя библиотека.
Пакет на npm: responsive-media
Репозиторий: macrulezru/responsive-media