Reactive boolean state from CSS media queries and element dimensions for Vanilla JS, Vue 3, Nuxt and React 19+ — with no required peer dependencies.

A CSS media query decides how the layout looks, but it tells JavaScript nothing — responsive-media turns that same media query into a plain reactive boolean you can read straight from a component's own logic, with no separate resize listener or manual debouncing.
Mobile shows five cards in a feed, desktop shows a twenty-column table. That's not a styling question — it's a different data set and different render logic, and the component reads the current breakpoint as a plain reactive value to decide what to render.
The same dashboard widget should collapse into a compact view in a narrow sidebar and expand fully in a wide center column. The widget tracks its own container size, not the whole screen, and switches on its own.
The system's "reduce motion" setting should actually turn off transitions and parallax on the site — it's exposed as the same kind of reactive value as any other breakpoint, and just as easy to subscribe to.
The server doesn't know the user's real screen size yet — state carries over from server to client safely, so the layout doesn't jump from one version to another right after the page loads.

Track viewport and element dimension changes through a unified API. ReactiveResponsiveState uses window.matchMedia for breakpoints, while ContainerState uses ResizeObserver to evaluate element size and orientation. The state is always up‑to‑date and updates without unnecessary re‑renders.

Describe breakpoints as an array of conditions (AND) or nested array of groups (OR). Supports min/max-width/height, orientation, aspect-ratio, prefers-color-scheme, hover, pointer, resolution, display-mode, and raw strings. This gives full control over responsiveness logic.

Subscribe to global changes (subscribe), specific keys (on), transitions (onEnter/onLeave), one‑time events (once), and wait for a condition (waitFor). Ordered breakpoints provide current, isAbove, isBelow, and between methods for semantic comparisons.

Sync state with CSS custom properties (syncCSSVars), emit DOM events (emitDOMEvents), turn keys into signals (toSignal), and pick values by the first active breakpoint (match). Use ready‑to‑use presets for Tailwind, Bootstrap, and accessibility.

Vue composables (useResponsive, useBreakpoints, useMediaQuery, useContainerState) and React hooks built on useSyncExternalStore provide reactivity in templates and components. The Nuxt module takes your breakpoints from nuxt.config.ts, generates their types, hydrates without mismatch warnings and can render the server for the visitor’s own device; defineResponsive infers the state keys from your config. All APIs are SSR‑safe, support hydration (including ssrState for the server layout), and have no required peer dependencies.

The library’s core has no framework dependency — the Vue and React integrations are thin wrappers around the same runtime, so the logic is just as reusable in plain vanilla code. createResponsiveState() creates isolated state instances, and hydrate() safely carries server state to the client.
responsiveState comes pre-configured for mobile/tablet/desktop — getState() gives a stable snapshot, proxy is live access with no debounce, and getResponsiveMediaQueries() returns the same conditions as ready-made CSS strings.
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 tracks a specific DOM element's size via ResizeObserver and toggles classes/CSS variables on its own — the same idea as CSS Container Queries, with support in browsers that don't have them natively.
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()