Skip to content

Responsive Media

v2.2.2UtilitiesVanilla JSVueNuxtReact

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

Responsive Media
Get started →
npm install responsive-media@latest
01 — Purpose

When you'd reach for this

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.

A component needs to know the breakpoint, not just CSS

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.

A widget responds to its container's size, not the screen's

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.

A user asked the browser not to show animations

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.

A page shouldn't flash the wrong layout for a split second

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.

02 — Features

At a glance

Reactive state from media and container queries

Reactive state from media and container queries

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.

Flexible configuration with AND/OR and many conditions

Flexible configuration with AND/OR and many conditions

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.

Rich subscription API and ordered breakpoints

Rich subscription API and ordered breakpoints

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.

Utilities, presets, and signal integration

Utilities, presets, and signal integration

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.

Ready‑to‑use integration with Vue 3, React 19+ and Nuxt

Ready‑to‑use integration with Vue 3, React 19+ and Nuxt

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.

A framework-agnostic core with safe hydration

A framework-agnostic core with safe hydration

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.

03 — Quick example

See how it works

One pre-configured singleton for the whole app

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.

singleton.ts
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)' }

Container queries, no framework required

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.

container-vanilla.ts
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()