~1.6 kB min+brotliZero dependenciesReact 17–19MIT

Device detection that's right in CSR and SSR

Know whether your user is on a phone, tablet, or desktop, and which OS they run. It catches the iPad that writes "Mac" into its own user agent, and it does not break in Next.js or anywhere else that builds HTML on the server first. No dependencies.

At a glance

Devices lie. The answers don't.

These are the devices where trusting the User-Agent string gets you the wrong answer.

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 stripped the model name out of the UA, so every Android just reports "K". Client Hints, which browsers send separately, still answer 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'

Has claimed to be a Mac since iPadOS 13. Five touch points give it away as a 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. One fact separates them: maxTouchPoints is 0.

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 does not change the answer. maxTouchPoints is only consulted to catch devices claiming to be Macs, so this 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 closest fit for a screen you drive with a remote from across the room.

Live demo

What actually happens during SSR

This page is HTML that Next.js built on the server. The left panel is 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, whatever device you arrive on. Server and browser start from the same value, so they cannot disagree.

Live values

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

The moment the browser takes over the page, one render fills in the real values. Touch and orientation keep following after that.

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 single boolean to running without React at all. You ship only what you import.

01

Read the full snapshot

One call gives you the device class, the OS, convenience booleans, and the values that change live. Server and browser always agree on the first paint, so you never write typeof window guards. Check isHydrated only when you want to hide the flash as the real values land.

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 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

Whatever you do not import is dropped at build time. Take only useIsMobile and useOS and the live-watching code disappears entirely, leaving ~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

The function that does the actual detecting is exported on its own. No React, no browser globals. Hand it a UA string and you get the same answer in middleware, on a server, or in a test.

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

export function middleware(request: Request) {
  // No React, no globals. Inject the 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

An iPad writing "Mac" into its user agent is still caught as a tablet, because maxTouchPoints is read alongside it. Android tablets follow the rule Google documents, and Samsung DeX and in-app webviews are handled too.

Client Hints first

On Chromium it reads Client Hints instead of the UA. Stripping the model name does not affect them. Only browsers without Client Hints fall back to parsing the UA string.

SSR-safe by construction

Server and browser use the same value on the first paint, so the two cannot disagree. That is why React 18/19 never log a hydration error here. The real values arrive one render later.

Tiny and tree-shakeable

No dependencies at all. Importing only useIsMobile ships ~1.1 kB, and the live-watching code never enters the bundle. CI checks these numbers.

Hybrid reactivity

Device class and OS stay pinned until the page reloads. Touch and orientation update live, so unfolding a foldable or attaching a keyboard to an iPad still gives the right answer.

Proven in real browsers

Beyond 98 unit tests, iPhone 15, iPad Pro, Galaxy S24, Galaxy Tab S9, and desktop Chrome and Safari are driven in real browsers to confirm the verdicts and zero errors.

Comparison

What about react-device-detect?

react-device-detect computes its values the moment you import it and freezes them. That breaks under server rendering. It calls iPads desktops, never updates, and because unused parts cannot be dropped it always ships ~13 kB. It has been unmaintained since 2023, and its parser dependency moved to AGPL. react-device-check is a maintained MIT replacement designed around how platforms behave today.

API

Small surface, full coverage

APIDescription
useDevice()Device class, OS, convenience booleans, and the live touch and orientation values in one call.
useDeviceType()'mobile' | 'tablet' | 'desktop'. Static per session, listener-free.
useIsMobile() · useIsTablet() · useIsDesktop()When one boolean is all you need. Importing only these drops the live-watching code from the bundle.
useOS()'ios' | 'android' | 'windows' | 'macos' | 'linux' | 'unknown'.
useDevicePixelRatio()The physical pixels per CSS pixel, for @2x assets and canvas scaling. Reactive, and 1 until hydration.
detectDevice(input?, options?)The detection function without React. Pass the values in yourself, which suits servers and tests.