Reference
Architecture
responsive-media
│
├── BaseResponsiveState (abstract)
│ Proxy over a plain Record<string, boolean>
│ Batched updates — all matchMedia/ResizeObserver callbacks collected
│ before a single notify() fires
│ Subscription registry: global listeners + per-key listeners
│ Debounce for subscribe(); key listeners never debounced
│ Ordered breakpoint helpers: current / isAbove / isBelow / between
│ Utilities: syncCSSVars / emitDOMEvents / toSignal / hydrate / destroy
│
├── ReactiveResponsiveState extends BaseResponsiveState
│ setupSources() → window.matchMedia per config key
│ Generated media query string stored in mediaQueries map
│ Batches all MediaQueryList 'change' events, then flushes
│ setConfig() / destroy() cleanupSources() removes matchMedia handlers
│
├── ContainerState extends BaseResponsiveState
│ setupSources() → single ResizeObserver on the target element
│ Evaluates width/height/orientation/aspect-ratio conditions in JS
│ Reconnects observer when config changes; teardown on destroy()
│
├── createResponsiveState(config, options)
│ Factory — returns a new ReactiveResponsiveState instance
│
├── responsiveState (global singleton)
│ Pre-configured with default ResponsiveConfig (mobile/tablet/desktop)
│ setResponsiveConfig() / getResponsiveState() / getResponsiveMediaQueries()
│ delegate to this instance
│
├── toMediaQueryString(conditions)
│ Converts MediaQueryConfig → CSS string
│ AND: flat array → joined with ' and '
│ OR: nested array → groups joined with ', '
│ Numeric values get 'px' suffix (except raw / orientation / …)
│
├── match(state, map, fallback?)
│ First-match lookup — returns mapped value for first true key in map
│
├── subscribeMediaQuery(query, callback)
│ Thin wrapper around window.matchMedia + addEventListener('change')
│ Returns cleanup function
│
├── Vue adapter (vue-responsive.ts)
│ RESPONSIVE_KEY — injection key; provide() it to hand useResponsive()/
│ useBreakpoints() a custom state instead of the global one
│ ResponsivePlugin — app.use(); calls setResponsiveConfig, provide()s RESPONSIVE_KEY
│ useResponsive() — inject(RESPONSIVE_KEY) first, else shallowReactive mirror; subscribe() syncs it
│ useBreakpoints() — returns { current, isAbove, isBelow, between }
│ also checks inject(RESPONSIVE_KEY) first, same as useResponsive()
│ useMediaQuery() — wraps subscribeMediaQuery in a ref + onUnmounted; returned ref also
│ always carries .stop() for cleanup outside a component instance
│ useContainerState() — watchEffect over templateRef; creates/destroys
│ ContainerState; returns shallowReactive mirror
│
├── React adapter (react-responsive.ts)
│ useResponsive() — useSyncExternalStore(subscribe, getState, getState)
│ useBreakpoints() — useSyncExternalStore + wraps ordered helpers
│ useMediaQuery() — useSyncExternalStore over subscribeMediaQuery
│ useContainerState() — useEffect creates ContainerState; useState mirror;
│ retries via requestAnimationFrame if the ref isn't attached yet
│
└── Presets (presets.ts)
ResponsiveConfig — default mobile/tablet/desktop
TailwindPreset / TailwindOrder
BootstrapPreset / BootstrapOrder
AccessibilityPresetBundle size & peer dependencies
| Entry point | Peer deps | Size (min + gzip) | Notes |
|---|---|---|---|
responsive-media | (none) | 12.5 kB + 4.3 kB | Framework-agnostic core only — no Vue or React code |
responsive-media/vue | vue ^3.5.27 | 12.0 kB + 4.2 kB | Vue composables only |
responsive-media/react | react ^19 | 10.3 kB + 3.5 kB | React hooks only |
responsive-media/presets | (none) | 1.3 kB + 0.4 kB | Preset configs only — add to any instance |
responsive-media/container | (none) | 6.8 kB + 2.4 kB | ContainerState class + factory only |
responsive-media/nuxt | @nuxt/kit | 0 in the browser | The Nuxt module — build-time only, no browser code |
responsive-media/testing | (none) | 3.8 kB + 1.7 kB | Test helpers — a controllable matchMedia |
Sizes are for the whole entry, minified and gzipped, with Vue, React and @nuxt/kit left out; they were measured on 2.2.2 (npm run size in the package repository repeats the measurement). Every entry carries the core it uses, so the sizes do not add up: an app that uses the Vue entry pays for it once, not for the root as well.
Typical imports
| Import | Size (min + gzip) |
|---|---|
createResponsiveState from the root | 6.2 kB + 2.1 kB |
useResponsive from responsive-media/vue | 6.8 kB + 2.4 kB |
useMediaQuery from responsive-media/vue | 0.6 kB + 0.4 kB |
defineResponsive from responsive-media/vue | 8.5 kB + 3.1 kB |
useResponsive from responsive-media/react | 6.4 kB + 2.2 kB |
The package is marked sideEffects: false, and the shared responsiveState lives in its own module, so a bundler drops it when nothing imports it. That is why useMediaQuery, which needs no shared state, weighs a fraction of a full entry; functions built on the state, such as useResponsive, still carry it.
The package ships as tree-shakeable ESM and CommonJS. As of 2.0, the Vue adapter (ResponsivePlugin, useResponsive, etc.) has its own entry point, responsive-media/vue, exactly like the React one — the main entry never imports anything Vue- or React-specific, so its footprint stays minimal for any consumer, framework or none. (Before 2.0, the Vue adapter was bundled into the main entry itself, which meant importing the package root at all pulled in a static @vue/runtime-core import — a real bug, not a documented tree-shaking behavior; if you see older example code importing Vue composables from bare 'responsive-media', update it to 'responsive-media/vue'.)
License
MIT