Skip to content

Nuxt Integration ​

The responsive-media/nuxt module turns your breakpoints into app-wide, typed composables. You describe them once in nuxt.config.ts; useResponsive(), useBreakpoints() and useResponsiveValue() are then auto-imported and know your breakpoint keys, with no generic to write and no wrapper composable to maintain. It also takes care of server rendering: a hydration that does not mismatch, and a server that can render for the visitor's device.

Installation ​

bash
npm install responsive-media

Register the module. With no options it uses the package's default mobile / tablet / desktop breakpoints:

ts
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['responsive-media/nuxt'],
})

Nuxt ^3.9.0 || ^4.0.0. The module needs @nuxt/kit, which Nuxt already provides.

Configuration ​

The module reads the responsive key of nuxt.config.ts, and the editor completes and checks it. Every option is optional, and every value must be plain data — it is written into a generated file.

  • breakpoints — Record<string, MediaQueryConfig> · default: the package's ResponsiveConfig (mobile, tablet, desktop). The same format as everywhere else — see Core Concepts.
  • order — string[] · default: the order of the keys in breakpoints. The order current, isAbove(), isBelow() and between() use.
  • debounce — number · default: 0. A delay in milliseconds before a change reaches the shared state's subscribe() listeners. The reactive state in Vue follows those listeners, so it is delayed too; 0 disables it.
  • ssrState — Record<string, boolean> · default: none. The state the server renders with, and the fallback for the hints below. Without it every key is false on the server.
  • hydration — 'immediate' | 'deferred' · default: 'deferred'. With 'deferred' the client hydrates with the same values the server used and switches to the real ones once the page has finished hydrating, so there is no hydration mismatch. See SSR & Hydration.
  • ssrHints — ('cookie' | 'user-agent')[] · default: []. How the server guesses the visitor's layout from the request, tried in this order. With none, the server always renders ssrState.
  • ssrDevices — Partial<Record<'mobile' | 'tablet' | 'desktop', { width: number; height: number }>> · default: 390x844, 820x1180, 1440x900. The sizes a user-agent hint is rendered for.
  • cookie — string · default: 'responsive-viewport'. The name of the cookie the cookie hint uses.
  • devBadge — boolean · default: false. A small badge in the corner of the page, in development only, with the current breakpoint and the window width.
  • css — { scss?: boolean; customMedia?: boolean } · default: none. Generates a stylesheet module from breakpoints — see CSS Integration.

Example:

ts
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['responsive-media/nuxt'],

  responsive: {
    breakpoints: {
      mobile: [{ type: 'max-width', value: 600 }],
      smallTablet: [{ type: 'max-width', value: 850 }],
      tablet: [{ type: 'max-width', value: 960 }],
      desktop: [{ type: 'min-width', value: 961 }],
    },
    order: ['mobile', 'smallTablet', 'tablet', 'desktop'],
    ssrState: { desktop: true },
    ssrHints: ['cookie', 'user-agent'],
    css: { scss: true },
  },
})

What the module registers ​

  • useResponsive(), useBreakpoints() and useResponsiveValue() — auto-imported, and typed from your breakpoints: useResponsive() returns { mobile: boolean; tablet: boolean; smallTablet: boolean; desktop: boolean }, useBreakpoints() accepts only those keys, and useResponsiveValue({ mobile: 1, tablet: 2 }) accepts only those keys too.
  • useMediaQuery(), useContainerState(), useUserPreferences() and useViewportSize() — auto-imported from responsive-media/vue. useMediaQuery() takes a string, a ref or a getter, and useContainerState() infers the keys of its own config.
  • A plugin — installs the shared state on the Vue app, so every component reads the same reactive state, and on the server gives every request its own.

Using the composables ​

vue
<script setup lang="ts">
const responsive = useResponsive()
const { current, isAbove } = useBreakpoints()
const columns = useResponsiveValue({ mobile: 1, tablet: 2, desktop: 4 })
const prefs = useUserPreferences()

const box = useTemplateRef<HTMLElement>('box')
const boxState = useContainerState(box, {
  narrow: [{ type: 'max-width', value: 300 }],
  roomy: [{ type: 'min-width', value: 301 }],
})
</script>

<template>
  <CompactLayout v-if="responsive.smallTablet" />
  <WideLayout v-else />

  <span>{{ current }}</span>
  <Grid :columns="columns" />
  <MobileOnly v-if="!isAbove('mobile')" />
  <div ref="box">{{ boxState.narrow ? 'narrow' : 'roomy' }}</div>
  <Moon v-if="prefs.dark" />
</template>

Types ​

The state and the breakpoint keys are inferred from breakpoints, so a typo is a compile error instead of a silent undefined:

ts
const responsive = useResponsive()
responsive.smallTablet // boolean
responsive.nope // error: Property 'nope' does not exist

const { isAbove } = useBreakpoints()
isAbove('smallTablet') // fine
isAbove('nope') // error: not assignable to 'mobile' | 'tablet' | 'smallTablet' | 'desktop'

useResponsiveValue({ nope: 1 }) // error: 'nope' does not exist in the breakpoints

The responsive key of nuxt.config.ts is typed as well: an unknown option, or a value such as hydration: 'sometimes', is reported by the editor.

SSR and hydration ​

The server does not know the size of the browser, and the module offers three levels of handling that — the full picture is in SSR & Hydration:

  • A fixed guess. ssrState is what the server renders. Choose the layout most visitors get, usually desktop.
  • No mismatch. With the default hydration: 'deferred' the browser hydrates with the server's values and switches to its real ones once the page has finished hydrating (when Nuxt reports app:suspense:resolve, so async components are covered). Vue logs no hydration warning, and the page renders twice.
  • A better guess. ssrHints: ['cookie', 'user-agent'] lets the server render for the visitor's own device:
    • cookie — the browser writes its window size to the responsive-viewport cookie (<width>x<height>) after mounting and when the window is resized, and the server reads it on the next request. A returning visitor gets the exact layout in the first HTML.
    • user-agent — the server classifies the device as mobile, tablet or desktop and renders for ssrDevices of that class.

The values the server used are put into the page payload, so the client hydrates with exactly them.

Caching. With hints on, the HTML depends on the Cookie and User-Agent headers. Make a CDN or proxy that caches pages vary on them (Vary: Cookie, User-Agent), or leave ssrHints empty. Prerendered pages have no request and always use ssrState.

Privacy. The cookie holds the window size of the browser; it is read by your server only.

useMediaQuery() and container state have no server value and are false or empty until the browser takes over. Put the parts that depend on them in <ClientOnly>.

Development badge ​

devBadge: true shows a small badge at the corner of the page in development — the current breakpoint and the window width, for example smallTablet · 800px. It is never part of a production build.

How it works ​

  • Generated files. The module writes .nuxt/responsive-media/index.ts, a call to defineResponsive() with your breakpoints and options, and a plugin that installs it. That file is what gives the composables their types.
  • One shared state, one per request. defineResponsive() applies the configuration to the package's shared state when the plugin loads. In the browser the plugin uses that state; on the server it creates a separate state for every request, so concurrent requests with different hints never see each other's values.
  • Transpiling. The module adds responsive-media to build.transpile, so the server build can load the package's ESM files.
  • Without the module. The Vue adapter works in Nuxt on its own — import useResponsive from responsive-media/vue and call defineResponsive() yourself — but then you register the plugin and write the types by hand.