Lightweight OS, form-factor, and runtime detection for browsers, Node.js, and SSR — with React hooks and Vue composables. No dependencies.

navigator.userAgent lies more often than you'd think: iPadOS pretends to be a Mac, and naive parsing catches Windows 10 where it's actually Windows 11 — os-detect takes on these edge cases instead of a hand-rolled regexp.
A "Ctrl+K" hint looks out of place on a Mac, and "⌘K" looks wrong on Windows. The package figures out which operating system it's running on, without parsing the user agent string by hand.
Since iPadOS 13, the browser reports itself as "Macintosh," and a naive check would mistake a tablet for a laptop. An extra check for touch support tells them apart correctly.
A new operating-system feature only works on Windows 11 — telling it apart from Windows 10 and older needs to work the same way in the browser and on the server.
Code that directly reads browser globals on the server just crashes during server-side rendering — OS detection stays safe for that case and updates itself once the work moves to the browser.
Windows should feel like Fluent, Android like Material — but a phone and a desktop browser on the same OS still want different layouts. OS and form factor together answer both questions instead of guessing from screen width alone.
A 2-in-1 laptop (or a foldable) can go from touch-only to mouse-primary without a page reload. Checking touch support once at load time misses that entirely — the primary-input check here updates itself live instead.

The getOS() function returns the current OS identifier: iOS, macOS, Android, Windows, Linux, ChromeOS, or unknown. Additional boolean functions (detectIsIOS, detectIsWindows, and others) let you flexibly check specific systems with no extra dependencies.

detectIsWindows11() asynchronously detects Windows 11 in the browser (via getHighEntropyValues) and in Node.js (via os.release). Returns true only for Windows 11, letting you distinguish it from Windows 10 and older without complex parsing.

isMobileDevice() and isDesktopDevice() provide quick coarse classification, and getFormFactor() goes further — phone, tablet, desktop, or TV, driven by OS and physical screen size rather than touch capability. The iOS detector correctly handles iPadOS 13+, which masquerades as Macintosh in the userAgent, using navigator.maxTouchPoints for accurate differentiation. Separate checks cover whether a touchscreen exists at all, which input type is primary right now, and the device pixel ratio.

useOS(), useFormFactor(), and useRuntime() (sync), plus useIsWindows11() (async with null state) are available for React and Vue. usePrimaryInput() is the one genuinely reactive hook of the set — it updates itself live if a hybrid device's keyboard or mouse gets attached or detached mid-session. React hooks use useState, Vue composables return readonly refs. All are SSR-safe.

getRuntime() tells a browser tab apart from a Node.js script or a Web Worker, and standalone checks narrow that further: whether the app is running inside Electron (main or renderer process), and whether it's installed and running as a standalone PWA rather than a regular browser tab.

In Node.js, the library reads process.platform, and every function caches its result after the first call — except the two whose answer can genuinely change mid-session (primary input type, pixel ratio), which stay live on purpose. Cache reset is available for testing. The package has no external dependencies, ships in ESM, CJS, and UMD, with React and Vue as optional peer dependencies.
getOS() and the device-category checks are cached and behave identically in the browser, in Node, and on the server — no separate server-only code path needed.
import { getOS, isMobileDevice, isDesktopDevice } from 'os-detect'
getOS() // 'windows' | 'macos' | 'ios' | 'android' | 'linux' | 'chromeos' | 'unknown'
isMobileDevice() // true on iOS or Android
isDesktopDevice() // true on macOS, Windows, Linux, or ChromeOS
// All synchronous and cached — and work identically in the browser, in
// Node, and during SSR, no separate server-only code path needed. Most OS detectors stop at the userAgent string and can't get past plain "Windows" — this one asks the browser's own Client Hints API (or os.release() in Node) to actually know.
import { detectIsWindows11 } from 'os-detect'
const isWin11 = await detectIsWindows11() // true only on Windows 11
// Most OS detectors stop at the userAgent string, which can't tell Windows
// 10 from 11 — this one asks the browser's own Client Hints API (or
// os.release() in Node) to actually know.getFormFactor() looks at OS and physical screen size, not touch capability — a touchscreen Windows laptop still comes back 'desktop', not 'tablet'.
import { getOS, getFormFactor } from 'os-detect'
if (getOS() === 'windows' && getFormFactor() === 'desktop') {
loadFluentDesignSystem()
} else if (getOS() === 'android') {
loadMaterialDesignSystem()
}
// getFormFactor() is driven by OS and physical screen size, not touch
// capability — a touchscreen Windows laptop still comes back 'desktop'.