npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

preact-device

v1.0.0-beta.0

Published

Modern, minimal device detection for Preact (and React via preact/compat). ~563B brotli, SSR-safe, tree-shakeable.

Readme

preact-device

Modern, minimal device detection for Preact (and React via preact/compat).

~700 B gzip   ·   1 hook   ·   1 function   ·   no peer deps

Why this exists

react-device-detect is the de-facto incumbent in this space. It's also:

  • ~54 KB raw / ~17 KB gzip (34 KB lib + 20 KB ua-parser-js dep)
  • Last published February 2023 — basically abandoned
  • CommonJS bundle, eager module-level evaluation — hard to tree-shake; importing isMobile pulls all 38 booleans, 12 view components, and the entire ua-parser-js regex table
  • No SSR strategy — top-level booleans are computed at import time. On the server they all evaluate against an empty UA → all false. Client hydration mismatches whenever real device ≠ desktop.
  • Class-based view components (<MobileView>, <IOSView>, …, 12 of them) that wrap {isMobile && children}. Idiomatic conditional rendering doesn't need a library for this.
  • React-only (uses React.Component, peer-deps react ≥ 0.14)

preact-device covers the same common cases (form factor + 5 OSes + 4 browsers + iPad-as-Mac feature detection) in <2% of the bundle size, with first-class SSR support and a hook-first API. It works with Preact directly and with React via preact/compat.

Comparison

| | react-device-detect | preact-device | | ------------------------ | ------------------- | ------------------ | | Bundle (raw) | 34 KB (+20 KB dep) | ~1.5 KB | | Bundle (gzip) | ~17 KB | ~0.7 KB | | Dependencies | ua-parser-js | none | | Peer deps | react, react-dom| preact | | Module format | CommonJS | ESM | | Tree-shakeable | No (module state) | Yes (no module-level state) | | SSR-safe | No (eager eval) | Yes (pure function + hydration-aware hook) | | iPad-as-Mac detection | Yes | Yes | | Form factor selectors | 8 | 5 (focused) | | OS selectors | 5 | 5 | | Browser selectors | 13 (incl. niche) | 4 + raw UA | | View components | 12 class components | 0 (use &&) | | Hooks | 3 | 1 | | Last meaningful update | 2023 | maintained |

Bundle impact

Measured with esbuild --bundle --minify --format=esm (preact externalized), then compressed:

| Format | Size | | ----------------- | --------- | | Minified JS | 1,225 B | | gzip -9 | 658 B | | brotli -q 11 | 563 B |

For comparison: react-device-detect itself is ~17 KB gzip (≈14 KB brotli) before its ua-parser-js dependency, plus another ~6 KB brotli for that. So preact-device is roughly 35× smaller for the cases both libraries cover.

Note on duplication across chunks. Because there's no module-level state and the lib is pure ESM, each consumer chunk that imports useDevice would get its own copy unless your bundler (Vite/Rollup) hoists it into a shared chunk. With one or two consumers, hoisting kicks in and you pay it once. With many isolated islands, worst case is ~563 B per island chunk. Still smaller than a single SVG icon — but worth knowing if you're shipping dozens of independent islands.

Install

npm install preact-device
# or in a Preact monorepo, copy lib/preact-device/index.ts and adjust the import path.

API

useDevice(ssrUA?: string): Device

Hook for use inside components. SSR-safe by design:

  • On the server (no navigator), returns sensible desktop defaults so the SSR'd HTML is deterministic.
  • On the client, re-evaluates with the real navigator after mount. Costs one extra render on hydration but never produces a hydration mismatch warning.
import { useDevice } from 'preact-device'

function Nav() {
  const { isMobile, isTouchDevice } = useDevice()
  return isMobile ? <MobileNav /> : <DesktopNav touch={isTouchDevice} />
}

For frameworks that pass UA from request headers (Astro, Remix, Next), pass it through to skip the post-hydration re-render:

// Astro server: `<Nav client:load ssrUA={Astro.request.headers.get('user-agent')!} />`
function Nav({ ssrUA }: { ssrUA?: string }) {
  const device = useDevice(ssrUA)
  // server and first client render agree → no hydration re-render
}

getDevice(ua?: string): Device

Pure synchronous function. Use outside components, in module-level code, in route guards, in service workers — anywhere you don't have hooks.

import { getDevice } from 'preact-device'

// Client (reads navigator.userAgent):
if (getDevice().isIOS) loadIOSPolyfill()

// SSR (pass UA explicitly):
export async function GET({ request }) {
  const device = getDevice(request.headers.get('user-agent') ?? undefined)
  return Response.json({ layout: device.isMobile ? 'compact' : 'full' })
}

Device shape

interface Device {
  // Form factor
  isMobile:       boolean   // phone OR tablet
  isTablet:       boolean   // tablet only (iPad-as-Mac counts)
  isPhone:        boolean   // phone only
  isDesktop:      boolean   // not mobile/tablet
  isTouchDevice:  boolean   // any touch input available (independent of form factor)

  // OS
  isIOS:          boolean   // includes iPad-as-Mac via maxTouchPoints check
  isAndroid:      boolean
  isMacOS:        boolean
  isWindows:      boolean
  isLinux:        boolean

  // Browser
  isSafari:       boolean
  isChrome:       boolean
  isFirefox:      boolean
  isEdge:         boolean
  isMobileSafari: boolean   // isSafari && isIOS — most "Safari quirk" code means this

  // Escape hatch
  ua:             string    // raw UA string for custom checks
}

Migration from react-device-detect

Most of react-device-detect's API maps cleanly:

| react-device-detect | preact-device | | ---------------------------- | ------------------------------------ | | import { isMobile } | useDevice().isMobile | | <MobileView>{x}</MobileView> | {useDevice().isMobile && x} | | <IOSView>{x}</IOSView> | {useDevice().isIOS && x} | | useDeviceSelectors(ua) | getDevice(ua) | | useDeviceData(ua) | getDevice(ua) | | parseUserAgent(ua) | getDevice(ua) | | setUserAgent(ua) | (not needed — pass to getDevice) | | withOrientationChange | use matchMedia('(orientation: portrait)') directly |

What's intentionally not included

These exist in react-device-detect but are out of scope here:

  • Niche browsers: isMIUI, isYandex, isSamsungBrowser, isOpera, isIE, isChromium, isEdgeChromium, isLegacyEdge, isElectron. If you need these, check device.ua directly with a regex — it's two lines.
  • Niche device types: isSmartTV, isConsole, isWearable, isEmbedded. Same answer — device.ua is exposed for custom matching.
  • Version strings: osVersion, browserVersion, fullBrowserVersion, engineName, mobileVendor, mobileModel. These require a full UA-parser table (~20 KB on its own) and are usually used for analytics, not runtime decisions. Use getDevice().ua + your own regex if you need them, or pull in ua-parser-js directly when accuracy matters more than bundle size.
  • useMobileOrientation: orientation is one line of native API:
    const isPortrait = window.matchMedia('(orientation: portrait)').matches
    Not worth wrapping in a hook.
  • View components: <MobileView>, <IOSView>, etc. Conditional rendering with && is the JSX idiom. A wrapper component just adds noise:
    // react-device-detect
    <MobileView>{content}</MobileView>
    // preact-device
    {isMobile && content}

If you genuinely need a 12-class-component API surface, stick with react-device-detect. For everyone else, preact-device covers the realistic 90% of use cases at 2% of the bundle cost.

Design decisions

Why no module-level boolean exports (export const isMobile = ...). They look convenient but they're a SSR landmine: they evaluate at import time, on whatever environment is importing them. On the server that's an empty navigator → all false → wrong layout sent to the client → hydration mismatch when the real device is anything other than desktop. The only way to get correct values is to call getDevice() from inside the render path or useDevice() from inside a hook.

Why a single Device object instead of individual selectors. Tree-shaking. With separate selectors, importing one pulls in all the OS/browser/form-factor logic anyway because they share the parsing layer. With one function returning everything, you import one function and it has zero unused-export overhead. The destructuring (const { isMobile } = useDevice()) gives you the same call-site ergonomics.

Why no view components. <MobileView>{x}</MobileView> is {isMobile && x} plus 12 class component definitions in the bundle. The shorter form is also more flexible (works with fragments, arrays, conditionals like {isMobile ? <A/> : <B/>}).

Why getDevice() instead of always using useDevice(). Hooks have render-cycle semantics. Module-level code, event handlers, async callbacks, and SSR routes don't. A pure function works everywhere; the hook is sugar for the common in-component case.

Why pure UA regex + minimal feature detection. ua-parser-js is 20 KB to handle ~300 browsers, most of which you'll never see. The big-four browsers and five major OSes account for >99% of traffic; a 30-line regex covers them. The one place feature detection actually matters (iPad spoofing as Mac) gets navigator.maxTouchPoints — the only modern reliable signal.

License

MIT