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
npm install responsive-mediaRegister the module. With no options it uses the package's default mobile / tablet / desktop breakpoints:
// 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'sResponsiveConfig(mobile,tablet,desktop). The same format as everywhere else — see Core Concepts.order—string[]· default: the order of the keys inbreakpoints. The ordercurrent,isAbove(),isBelow()andbetween()use.debounce—number· default:0. A delay in milliseconds before a change reaches the shared state'ssubscribe()listeners. The reactive state in Vue follows those listeners, so it is delayed too;0disables it.ssrState—Record<string, boolean>· default: none. The state the server renders with, and the fallback for the hints below. Without it every key isfalseon 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 rendersssrState.ssrDevices—Partial<Record<'mobile' | 'tablet' | 'desktop', { width: number; height: number }>>· default:390x844,820x1180,1440x900. The sizes auser-agenthint is rendered for.cookie—string· default:'responsive-viewport'. The name of the cookie thecookiehint 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 frombreakpoints— see CSS Integration.
Example:
// 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()anduseResponsiveValue()— auto-imported, and typed from yourbreakpoints:useResponsive()returns{ mobile: boolean; tablet: boolean; smallTablet: boolean; desktop: boolean },useBreakpoints()accepts only those keys, anduseResponsiveValue({ mobile: 1, tablet: 2 })accepts only those keys too.useMediaQuery(),useContainerState(),useUserPreferences()anduseViewportSize()— auto-imported fromresponsive-media/vue.useMediaQuery()takes a string, a ref or a getter, anduseContainerState()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
<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:
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 breakpointsThe 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.
ssrStateis what the server renders. Choose the layout most visitors get, usuallydesktop. - 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 reportsapp: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 theresponsive-viewportcookie (<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 asmobile,tabletordesktopand renders forssrDevicesof 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 todefineResponsive()with yourbreakpointsand 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-mediatobuild.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
useResponsivefromresponsive-media/vueand calldefineResponsive()yourself — but then you register the plugin and write the types by hand.