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

07.03.2026
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 Copy
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 Copy
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 Copy
mobile: [
  { type: 'max-width', value: 500 },
  { type: 'orientation', value: 'portrait' },
]

Получится медиа-запрос (max-width: 500px) and (orientation: portrait).


Использование без Vue: состояние и подписка

Ядро даёт глобальный синглтон responsiveState. Через него вы читаете текущее состояние и подписываетесь на изменения.

Чтение состояния — через responsiveState.proxy: объект с булевыми флагами по каждому брейкпоинту. В какой момент активен ровно один из «ширинных» брейкпоинтов — зависит от конфига (при пересекающихся условиях могут быть несколько true).

ts Copy
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 Copy
const unsubscribe = responsiveState.subscribe((state) => {
  console.log('State changed:', state);
  // state.mobile, state.tablet, state.desktop
});

// когда не нужен списокенр:
unsubscribe();

Внутри используется window.matchMedia и событие change, так что обновление происходит при изменении размера окна, ориентации или других заданных в конфиге медиа-условий.


Кастомные брейкпоинты и комбинированные условия

Чтобы заменить дефолты или добавить свои брейкпоинты, используется setResponsiveConfig. В конфиг можно передать и дефолт (размазав ResponsiveConfig), и переопределения, и новые ключи.

Пример: свои границы и новый брейкпоинт «широкий экран»

ts Copy
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 Copy
setResponsiveConfig({
  ...ResponsiveConfig,
  mobile: [
    { type: 'max-width', value: 600 },
    { type: 'orientation', value: 'portrait' },
  ],
});

Строки медиа-запросов для CSS или отладки

Иногда нужно получить готовую строку медиа-запроса — например, для передачи в утилиту, генерации CSS или логов. Для этого служит getResponsiveMediaQueries(): возвращает объект «имя брейкпоинта → строка запроса».

ts Copy
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 Copy
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 Copy
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 Copy
<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 Copy
<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 задаётся строкой. Для ширины/высоты число автоматически превращается в пиксели (например 600600px).


Примеры под реальные задачи

1. Условный рендер и мобильное меню

Задача: на мобилке показывать кнопку «Меню», на десктопе — полноценную навигацию. Один источник правды для «мы на мобилке».

vue Copy
<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 Copy
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 Copy
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

Читать далее

07.03.2026

css-magic-gradient — генерация CSS-градиентов с реактивностью для Vue 3

Пакет css-magic-gradient генерирует линейные, радиальные и конические CSS-градиенты по базовому цвету и опциям, во Vue 3 композиции принимают ref цвета и опций и возвращают вычисляемую строку — при смене темы или выборе цвета градиент обновляется сам, без ручных подписок и watchers.

Метки
css-magic-gradientVue 3CSSградиентыреактивность
07.03.2026

color-value-tools — конвертация и манипуляция цветами в любых форматах

Библиотека color-value-tools — один набор утилит для разбора, конвертации и манипуляции цветом: hex, RGB, HSL, HSV, Lab, LCH, CMYK, яркость, смешивание и контраст по WCAG. В посте — зачем выносить работу с цветом в отдельный слой, обзор API и примеры под реальные задачи (темы, градиенты, доступность, печать).

Метки
color-value-toolsцветаконвертацияTypeScriptдоступность (WCAG)
13.03.2026

Toolz: генерируем полный набор фавиконок

Новый модуль Fav Icona в составе toolz.macrulez.ru закрывает задачу, с которой сталкивается каждый вебмастер при запуске проекта: подготовка полного набора favicon-файлов для всех платформ. Один SVG или PNG на входе — готовый пакет с десятком файлов, манифестом и HTML-кодом на выходе.

Метки
faviconвеб-разработкаPWAиконкиинструменты