~1.5 kB min+brotliZero dependenciesReact 17–19MIT

Device detection that’s right in CSR and SSR

In React CSR apps and Next.js SSR alike, know exactly whether your user is on a phone, tablet, or desktop, and which OS. Zero dependencies, iPads unmask themselves, frozen user agents don’t matter, and there is not a single hydration error.

At a glance

Devices lie. The answers don’t.

Each screen shows what useDevice() returns on that device — including the ones that lie about themselves.

type: 'mobile'
os: 'ios'
isTouchPrimary: true
orientation: 'portrait'

iPhone 15

What it claimsMacintosh; Intel Mac OS X · screen: 393px
What the hook returns'mobile''ios'

With Safari’s “Request Desktop Website” even an iPhone claims to be a Mac. Multitouch plus the screen-size cross-check still says mobile.

type: 'mobile'
os: 'android'
isTouchPrimary: true
orientation: 'portrait'

Galaxy S24

What it claimsLinux; Android 14; K
What the hook returns'mobile''android'

Chrome froze the UA — every Android reports model “K”. Client Hints still nail it.

type: 'tablet'
os: 'ios'
isTouchPrimary: true
orientation: 'landscape'

iPad Pro

What it claimsMacintosh; Intel Mac OS X 10_15_7
What the hook returns'tablet''ios'

Masquerades as a Mac since iPadOS 13. Multitouch unmasking says tablet anyway.

type: 'desktop'
os: 'macos'
isTouchPrimary: false
orientation: 'landscape'

iMac

What it claimsMacintosh; Intel Mac OS X 10_15_7 · maxTouchPoints: 0
What the hook returns'desktop''macos'

A real Mac sending the exact same UA as the iPad above. The multitouch cross-check (maxTouchPoints: 0) is what tells them apart.

type: 'desktop'
os: 'windows'
isTouchPrimary: false
orientation: 'landscape'

Windows touch laptop

What it claimsWindows NT 10.0 · maxTouchPoints: 10
What the hook returns'desktop''windows'

A touchscreen doesn’t fool it — touch is only consulted for the Apple masquerade. Stays desktop.

type: 'desktop'
os: 'android'
isTouchPrimary: false
orientation: 'landscape'

Android TV

What it claimsLinux; Android 11; SHIELD Android TV
What the hook returns'desktop''android'

An Android UA without the Mobile token would normally land in the tablet bucket, but TV markers are checked first — desktop is the best fit for a 10-foot UI.

Live demo

Watch the SSR contract in action

This page is server-rendered by Next.js. The left panel is frozen at the hydration first paint — exactly what the server sent. The right panel is what the hook knows right now.

First paint (what the server rendered)

server default
type
desktop
os
unknown
isTouchPrimary
false
orientation
landscape
isHydrated
false

Always desktop / unknown, on every device — that is why server HTML and client HTML can never disagree.

Live values

server default
type
desktop
os
unknown
isTouchPrimary
false
orientation
landscape
isHydrated
false

Corrected in a single render right after hydration. isTouchPrimary and orientation keep updating live.

Open this page on a phone, or reload with DevTools device emulation: the left panel stays desktop while the right one tells the truth — and the console logs zero hydration errors.

Usage

Three ways to use it

From a one-line boolean to the framework-free engine — each import ships only what it actually needs.

01

Read the full snapshot

useDevice() returns type, os, boolean sugar, and the live fields. The server render and the hydration first paint always agree by construction, so you never write typeof window guards — branch on isHydrated only when you want to hide the one-render correction.

app/page.tsx
import { useDevice } from 'react-device-check';

export default function Page() {
  const { type, os, isMobile, isHydrated } = useDevice();

  // Server render & hydration first paint: type = 'desktop', isHydrated = false.
  // One render later the real device shows up — no hydration mismatch, ever.
  if (!isHydrated) return <Skeleton />;

  if (isMobile && os === 'ios') return <AppStoreBanner />;
  return <p>You are on a {type} running {os}.</p>;
}
02

Import only what you ship

The static hooks are listener-free and maximally tree-shakeable: importing only useIsMobile and useOS drops the reactive store entirely and ships ~1.1 kB. Perfect for OS-specific store buttons.

components/DownloadButton.tsx
import { useIsMobile, useOS } from 'react-device-check';

export default function DownloadButton() {
  const isMobile = useIsMobile(); // boolean only — ~1.1 kB total
  const os = useOS(); // 'ios' | 'android' | ...

  if (isMobile && os === 'ios') return <AppStoreButton />;
  if (isMobile && os === 'android') return <PlayStoreButton />;
  return <DesktopDownloadButton />;
}
03

Use the engine anywhere

detectDevice() is the pure decision tree behind the hooks — no React, no globals. Inject a UA string (or Client Hints) and get the same deterministic verdict in middleware, on servers, or in tests.

middleware.ts
import { detectDevice } from 'react-device-check';

export function middleware(request: Request) {
  // No React, no globals — inject any signals you have
  const { type } = detectDevice({
    ua: request.headers.get('user-agent') ?? '',
  });

  if (type === 'mobile') {
    // e.g. rewrite to the lightweight variant
  }
}

Why

Built for how devices lie in 2026

Accurate where others fail

iPads report as tablets even behind the macOS desktop UA (MacIntel + multitouch unmasking). Android tablets follow the official Mobile-token rule; Samsung DeX and in-app WebViews are handled.

Client Hints first

Trusts navigator.userAgentData on Chromium — immune to user-agent freezing — and falls back to UA parsing everywhere else.

SSR-safe by construction

Server render and hydration first paint always match, so React 18/19 never log a hydration mismatch. The hook corrects itself in one post-hydration render.

Tiny and tree-shakeable

Zero runtime dependencies, dual ESM/CJS. Importing only useIsMobile ships ~1.1 kB and drops the reactive store entirely — budgets are enforced in CI.

Hybrid reactivity

type and os stay stable for the session, while isTouchPrimary and orientation update live via matchMedia — covering foldables, DeX docking, and iPad Stage Manager.

Proven in real browsers

Beyond 76 unit tests, a Playwright matrix — iPhone 15, iPad Pro, Galaxy S24, Galaxy Tab S9, desktop Chrome and Safari — verifies detection and zero hydration errors.

Comparison

What about react-device-detect?

react-device-detect computes import-time constants from the UA, which crashes or mismatches under SSR, misreports iPads as desktops, never updates, and ships ~13 kB gzip that cannot be tree-shaken. It has been unmaintained since 2023, and its parser dependency moved to AGPL. react-device-check is a maintained MIT replacement designed around today’s platform realities.

API

Small surface, full coverage

APIDescription
useDevice()Full snapshot: type, os, boolean sugar, live isTouchPrimary / orientation, and isHydrated.
useDeviceType()'mobile' | 'tablet' | 'desktop' — static per session, listener-free.
useIsMobile() · useIsTablet() · useIsDesktop()Boolean sugar — maximally tree-shakeable; importing only these drops the reactive store.
useOS()'ios' | 'android' | 'windows' | 'macos' | 'linux' | 'unknown'.
detectDevice(input?, options?)The pure engine — no React required, every signal injectable. Great for servers and tests.