Лёгкое определение ОС, форм-фактора и рантайм-окружения для браузера, Node.js и SSR — с React-хуками и Vue composables. Без зависимостей.

navigator.userAgent врёт чаще, чем кажется: iPadOS выдаёт себя за Mac, а наивный парсинг ловит Windows 10 там, где на самом деле Windows 11 — os-detect берёт эти частные случаи на себя вместо самодельного regexp.
Подсказка «Ctrl+K» выглядит нелепо на Mac, а «⌘K» — на Windows. Пакет сам определяет, какая операционная система перед ним, без ручного разбора строки браузера.
Начиная с iPadOS 13 браузер представляется как «Macintosh», и наивная проверка спутает планшет с ноутбуком. Дополнительная проверка поддержки сенсорного экрана различает их корректно.
Новая возможность операционной системы работает только в Windows 11 — нужно отличить её от Windows 10 и более старых версий, причём одинаково и в браузере, и на сервере.
Код, который напрямую читает объекты браузера на сервере, просто упадёт при серверном рендеринге — определение ОС остаётся безопасным для такого сценария и само обновляется, когда работа переходит в браузер.
Windows должна ощущаться как Fluent, Android — как Material, но телефон и десктопный браузер на одной и той же ОС всё равно хотят разную вёрстку. ОС и форм-фактор вместе отвечают на оба вопроса, вместо того чтобы гадать по ширине экрана.
Ноутбук-трансформер (или складное устройство) может перейти от чисто сенсорного управления к мыши как основному вводу без перезагрузки страницы. Проверка поддержки тача один раз при загрузке этого не заметит — проверка активного типа ввода здесь обновляется сама, вживую.

Функция getOS() возвращает идентификатор текущей ОС: iOS, macOS, Android, Windows, Linux, ChromeOS или unknown. Дополнительные булевы функции (detectIsIOS, detectIsWindows и другие) позволяют гибко проверять конкретные системы без лишних зависимостей.

detectIsWindows11() асинхронно определяет Windows 11 в браузере (через getHighEntropyValues) и в Node.js (через os.release). Возвращает true только для Windows 11, позволяя отличить её от Windows 10 и более старых версий без сложных парсингов.

isMobileDevice() и isDesktopDevice() дают быструю грубую классификацию, а getFormFactor() идёт дальше — телефон, планшет, десктоп или телевизор, по ОС и физическому размеру экрана, а не по наличию тачскрина. Детектор iOS корректно обрабатывает iPadOS 13+, которая маскируется под Macintosh в userAgent, используя navigator.maxTouchPoints. Отдельные проверки — есть ли вообще сенсорный экран, какой тип ввода сейчас основной, и соотношение физических и логических пикселей.

useOS(), useFormFactor() и useRuntime() (синхронные), а также useIsWindows11() (асинхронный, с состоянием null) доступны для React и Vue. usePrimaryInput() — единственный по-настоящему реактивный хук из набора: сам обновляется, если у гибридного устройства прямо во время сессии подключили или отключили клавиатуру или мышь. React-хуки используют useState, Vue-композаблы возвращают readonly-рефы. Все они SSR-безопасны.

getRuntime() отличает вкладку браузера от Node.js-скрипта или Web Worker, а отдельные проверки уточняют это дальше: запущено ли приложение внутри Electron (в основном или renderer-процессе), и установлено ли оно и запущено как самостоятельное PWA, а не обычная вкладка браузера.

В Node.js библиотека читает process.platform, и каждая функция кэширует результат после первого вызова — кроме двух, чей ответ может по-настоящему измениться прямо во время сессии (активный тип ввода, соотношение пикселей), они намеренно остаются живыми. Есть сброс кэша для тестов. Пакет не имеет внешних зависимостей, поставляется в ESM, CJS и UMD, а React и Vue — опциональные peer-зависимости.
getOS() и проверки категории устройства кэшируются и работают одинаково в браузере, в Node и на сервере — отдельный серверный код-путь не нужен.
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. Большинство детекторов ОС смотрят только на userAgent и дальше «Windows» не идут — этот дёргает Client Hints API браузера (или os.release() в Node), чтобы узнать версию по-настоящему.
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() смотрит на ОС и физический размер экрана, а не на наличие сенсорного экрана — сенсорный ноутбук на Windows всё равно вернёт 'desktop', не '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'.