CLI tool
nuxt-modules/device avatar
nuxt-modules/device

@nuxtjs/device turns one request header into seventeen booleans

Nuxt module for detecting device type.

1,005 stars55 forksTypeScriptMIT

At a glance

What is it?
The Nuxt Device module is a MIT licensed Nuxt module that reads the user agent of the incoming request and exposes flags for device type, operating system, browser and crawler, either through a $device helper in the template or a useDevice() composable. It also understands the device headers that CloudFront and Cloudflare set, and it is explicit that static generation cannot work, because a prerendered page has no user agent at all.
Who is it for?
@nuxtjs/device suits a Nuxt application that needs to know what kind of client is asking, whether that is to pick a layout, to serve a different shell, or to keep a crawler out of a page, and that serves the app with nuxt build rather than nuxt generate. It does not suit a static site, since a prerendered page has no user agent and is always treated as a desktop, and it is the wrong tool for a layout that should follow the viewport rather than the device.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Sixteen flags, and the fallback rule for a missing header

The module's surface is a list of flags and a user agent string, and the flags are grouped by what they tell you.

Device class comes first: isDesktop, isMobile, isTablet, isMobileOrTablet and isDesktopOrTablet. Operating system comes next: isIos, isLinux, isWindows, isMacOS, isApple and isAndroid. Browser is next: isFirefox, isEdge, isChrome and isSafari. And two device vendor and type checks close the list: isSamsung and isCrawler.

The user agent itself is injected and accessible as $device.userAgent, so a caller that needs a detail the flags do not cover can still get the raw string.

Two behaviours are worth knowing before anything else. The first is the fallback for a request with no User-Agent header: $device.userAgent is an empty string and the device is treated as a desktop with an unknown OS and browser. That is a deliberate default, and it is a sensible one, since the alternative is to treat an unknown client as a mobile device and hide functionality from crawlers and API clients.

The second is the crawler check, which is powered by the crawler-user-agents package rather than by a pattern the module writes itself. That is a better source of truth, because the list of crawler user agents is a moving target and maintaining it in a Nuxt module would be a waste of effort.

The audience is a Nuxt developer who needs a server-side answer about the client, which is a different question from the client-side answer a media query gives.

Two ways to read the flags, and the template helper is the point

The module can be consumed two ways, and the README shows both.

There is the useDevice() composable, which you call inside a script setup block, and there is the $device helper, which is usable directly in the template:

vue
<template>
  <div>
    <div v-if="$device.isDesktop">Desktop</div>

    <div v-else-if="$device.isTablet">Tablet</div>

    <div v-else>Mobile</div>
  </div>
</template>

<script setup>
const { isMobile } = useDevice()
</script>

The template helper is the more useful of the two for most applications, because the typical use is a conditional in markup, and a global helper keeps that conditional free of setup boilerplate. The composable is there for when the flag is needed in logic rather than in a template, and the example destructures isMobile out of it.

The second example in the README is the one that shows what the module is for. The layout name is chosen from a flag:

vue
<template>
  <div>
    <NuxtLayout :name="$device.isMobile ? 'mobile' : 'default'">
      <!-- page content -->
    </NuxtLayout>
  </div>
</template>

<script setup>
definePageMeta({
  layout: false
})
</script>

The definePageMeta call with layout false is required, and it is easy to miss. It disables the layout Nuxt would otherwise apply, so that the component can pick one itself from the device flag.

Static generation is the trap, and the README says so plainly

The section on static generation is the most important in the README, and it is phrased as a prohibition. Static generation, for example with nuxt generate, is not supported. Prerendered pages are rendered once at build time without a user agent, so they are always treated as a desktop.

The reason is obvious once stated, and it is the kind of thing that produces a bug report months later. A statically generated site has one rendering, and that rendering happened on a build machine with no User-Agent header, so the fallback rule applies and everything is a desktop forever.

The same issue has a second form on a server-rendered site. Responses cached on the server, for example with swr or isr route rules, keep the flags of the request they were cached from. A mobile user served a cached desktop response sees the desktop layout, and the module cannot tell that happened, because by the time it runs there is only the cache entry.

There are two documented ways out. To detect the device on each request, deploy with nuxt build instead of nuxt generate. And for a client-side only application, set ssr: false so that the detection happens in the browser instead, where a user agent always exists.

The second option is worth understanding properly, because it is not a workaround. With ssr: false the whole application is client rendered, so the first paint is empty and the device is known only once the JavaScript has loaded. That trades the server's ability to serve a correct first response for the ability to know the device at all, and the choice between the two depends on whether the first paint has to be right for a search engine or a crawler, which is exactly the case the crawler flag exists to handle.

On a CDN, the header beats the user agent

The module handles two CDN cases explicitly, and the approach in both is the same: when the user agent is unhelpful, the edge provider's own device header is more accurate than the string.

For Amazon CloudFront, if the user agent is Amazon CloudFront, the module checks a set of headers: CloudFront-Is-Android-Viewer, CloudFront-Is-Desktop-Viewer, CloudFront-Is-IOS-Viewer, CloudFront-Is-Mobile-Viewer and CloudFront-Is-Tablet-Viewer. CloudFront sets these itself based on what it knows about the request, which is generally more reliable than parsing a user agent that CloudFront has replaced with its own name.

There is a caution attached, and it is specific. The isWindows and isMacOS flags are not available in Amazon CloudFront. The reason is implicit in how the CloudFront headers work, since the viewer headers describe a device class rather than an operating system, so the module has nothing to derive the OS from in that path. If an application branches on isWindows, it will not behave as expected behind CloudFront.

Cloudflare is handled by checking the CF-Device-Type header, and the README links the Cloudflare documentation for the device type detection used by their automatic platform optimization.

The general point for an adopter is that this module is request scoped and CDN aware, and that a different CDN, or a proxy that rewrites the user agent, will behave differently. The flags are derived from whatever the request looks like at the moment the module runs, which is exactly where the caching and prerendering problems above come from.

The repository is a Nuxt module with the tooling to match

The package metadata shows a module built the current way, and it is worth looking at because the tooling choices tell you how the project is maintained.

The package is @nuxtjs/device at version 4.0.0, of type module, and its exports map has an entry for the module itself and a wildcard for ./runtime/*, with matching typesVersions entries, and the published files are dist. The only runtime dependency is @nuxt/kit, so the module brings nothing to your dependency tree beyond the Nuxt kit itself.

The build uses nuxt-module-build, and the scripts show a complete development loop: a dev script that runs the playground, a dev:build, and a dev:prepare that stubs the module, prepares it and prepares the playground, plus a separate dev:v5 script that runs a second playground. There are two playgrounds, playground/ and playground-v5/, and the v5 one is prepared with a separate pnpm invocation, which is the shape of a project that supports two Nuxt generations at once.

The test setup is a proper one. Vitest runs the unit tests with vitest run, the type tests use vue-tsc with --noEmit across the package and the playground, and the linter is eslint with a cache. The release script chains them: lint, test, prepack, then changelogen --release, then publish and a tag push.

Around that are the files a maintained module usually carries: a build.config.ts, an eslint.config.mjs, a vitest.config.ts, a pnpm workspace with a lockfile, renovate.json for dependency updates, a CHANGELOG.md, and a .stackblitzrc, which is what makes the StackBlitz playground link in the README work.

Version 4 and what to check before you depend on it

The release history shows a major version recently, with v4.0.0 in December 2025, and before that v3.2.4 in September 2024 and v3.2.3 a couple of days earlier. The repository is not archived and the last push was on 2026-10-19, so the module is being worked on even though the recent published versions are sparse.

The module builder and the kit dependency being on the Nuxt 4 line, at @nuxt/kit ^4.6.1, tells you which generation the current release targets, and the presence of a v5 playground suggests the project is testing against the next generation as well.

The licence is MIT, and the package is published to npm under the @nuxtjs scope.

If you are evaluating this module, the checks in order of importance are these. First, confirm that you are not generating a static site, because that use is not supported and will always report a desktop. Second, check whether any of your routes use cached rendering, since those will serve one request's flags to everyone. Third, if you are behind CloudFront or Cloudflare, confirm which headers your configuration actually passes through, because the module's accuracy in that path depends entirely on them, and note the isWindows and isMacOS gap in the CloudFront case. Fourth, if you are choosing between this and a viewport based approach, decide whether the decision you are making is about the device or about the screen, because a device flag and a media query answer different questions and a large tablet and a small laptop can disagree.

The module is small, MIT licensed and does one thing, and the documentation is unusually clear about where it does not work, which is the main reason it is worth reading before you install it.

Editorial conclusion

@nuxtjs/device suits a Nuxt application that needs to know what kind of client is asking, whether that is to pick a layout, to serve a different shell, or to keep a crawler out of a page, and that serves the app with nuxt build rather than nuxt generate. It does not suit a static site, since a prerendered page has no user agent and is always treated as a desktop, and it is the wrong tool for a layout that should follow the viewport rather than the device. Verify first that you are not using nuxt generate or a cached route rule, that your CDN passes through the headers the module reads, and that the flags you rely on are not among the two that CloudFront requests do not provide.

Frequently asked questions

How do I install the Nuxt device module?

With npx nuxt module add device. The package is published to npm as @nuxtjs/device and its only runtime dependency is @nuxt/kit.

Does the Nuxt device module work with nuxt generate?

No. Static generation is not supported, because prerendered pages are rendered once at build time without a user agent and are therefore always treated as a desktop. The README advises deploying with nuxt build, or setting ssr: false for a client-side only app.

What happens when a request has no User-Agent header?

$device.userAgent is an empty string, and the device is treated as a desktop with an unknown operating system and browser.

How does the module handle CloudFront and Cloudflare?

For CloudFront, when the user agent is Amazon CloudFront it reads the CloudFront-Is-Android-Viewer, -Desktop-Viewer, -IOS-Viewer, -Mobile-Viewer and -Tablet-Viewer headers, and for Cloudflare it reads the CF-Device-Type header. The README cautions that isWindows and isMacOS are not available in the CloudFront case.

Do cached route rules affect the device flags?

Yes. Responses cached on the server, for example with swr or isr route rules, keep the flags of the request they were cached from, so a cached response can serve one device's flags to every visitor.

Official sources

  1. Issues
  2. License: MIT
  3. nuxt-modules/device on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/nuxt-modules-device.svg)](https://hysenlabs.com/projects/nuxt-modules-device)

Was this useful?