Subscribe
Join our newsletter to stay up to date on new releases.
eu1
26230392
15314855-8a4c-4751-ad29-605050b96984
loft-sh
true
By subscribing you agree to our Privacy Policy and provide email consent
© 2025 LoftLabs, Inc. All rights reserved.
<h3>native navigator.userAgentData</h3> | |
<pre><code id="naviveUserAgentData">...</code></pre> | |
<h3>polyfilled navigator.userAgentData</h3> | |
<pre><code id="customUserAgentData">...</code></pre> | |
<script type="module"> | |
import {ponyfill, polyfill} from './user-agent-data.js'; | |
const $ = (s, c = document) => c.querySelector(s); | |
function main() { | |
if (location.protocol !== 'https:') { | |
$('#naviveUserAgentData').textContent = 'navigator.userAgentData is not available in insecure context'; | |
$('#customUserAgentData').textContent = 'navigator.userAgentData polyfill is designed to work in secure context'; | |
return; | |
} | |
let keys = ['platformVersion', 'architecture', 'bitness', 'model', 'fullVersionList']; | |
if (!navigator.userAgentData) { | |
$('#naviveUserAgentData').textContent = 'Your browser doesn\'t support client hints'; | |
} else { | |
navigator.userAgentData.getHighEntropyValues(keys).then((result) => { | |
$('#naviveUserAgentData').textContent = JSON.stringify(result, null, 2); | |
}); | |
} | |
let userAgentData = ponyfill(); | |
userAgentData.getHighEntropyValues(keys).then((result) => { | |
$('#customUserAgentData').textContent = JSON.stringify(result, null, 2); | |
}); | |
} | |
document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', main) : main(); | |
</script> |
function getClientHints(navigator) { | |
let {userAgent} = navigator; | |
let mobile, platform = '', platformVersion = '', architecture = '', bitness = '', model = '', uaFullVersion = '', fullVersionList = []; | |
let platformInfo = userAgent; | |
let found = false; | |
let versionInfo = userAgent.replace(/\(([^)]+)\)?/g, ($0, $1) => { | |
if (!found) { | |
platformInfo = $1; | |
found = true; | |
} | |
return ''; | |
}); | |
let items = versionInfo.match(/(\S+)\/(\S+)/g); | |
let webview = false; | |
// detect mobile | |
mobile = userAgent.indexOf('Mobile') !== -1; | |
let m; | |
let m2; | |
// detect platform | |
if ((m = /Windows NT (\d+(\.\d+)*)/.exec(platformInfo)) !== null) { | |
platform = 'Windows'; | |
// see https://docs.microsoft.com/en-us/microsoft-edge/web-platform/how-to-detect-win11 | |
let nt2win = { | |
'6.1': '0.1', // win-7 | |
'6.2': '0.2', // win-8 | |
'6.3': '0.3', // win-8.1 | |
'10.0': '10.0', // win-10 | |
'11.0': '13.0', // win-11 | |
}; | |
let ver = nt2win[m[1]]; | |
if (ver) | |
platformVersion = padVersion(ver, 3); | |
if ((m2 = /\b(WOW64|Win64|x64)\b/.exec(platformInfo)) !== null) { | |
architecture = 'x86'; | |
bitness = '64'; | |
} | |
} else if ((m = /Android (\d+(\.\d+)*)/.exec(platformInfo)) !== null) { | |
platform = 'Android'; | |
platformVersion = padVersion(m[1]); | |
if ((m2 = /Linux (\w+)/.exec(navigator.platform)) !== null) { | |
if (m2[1]) { | |
m2 = parseArch(m2[1]); | |
architecture = m2[0]; | |
bitness = m2[1]; | |
} | |
} | |
} else if ((m = /(iPhone|iPod touch); CPU iPhone OS (\d+(_\d+)*)/.exec(platformInfo)) !== null) { | |
// see special notes at https://www.whatismybrowser.com/guides/the-latest-user-agent/safari | |
platform = 'iOS'; | |
platformVersion = padVersion(m[2].replace(/_/g, '.')); | |
} else if ((m = /(iPad); CPU OS (\d+(_\d+)*)/.exec(platformInfo)) !== null) { | |
platform = 'iOS'; | |
platformVersion = padVersion(m[2].replace(/_/g, '.')); | |
} else if ((m = /Macintosh; (Intel|\w+) Mac OS X (\d+([_.]\d+)*)/.exec(platformInfo)) !== null) { | |
platform = 'macOS'; | |
platformVersion = padVersion(m[2].replace(/_/g, '.')); | |
} else if ((m = /Linux/.exec(platformInfo)) !== null) { | |
platform = 'Linux'; | |
platformVersion = ''; | |
// TODO | |
} else if ((m = /CrOS (\w+) (\d+(\.\d+)*)/.exec(platformInfo)) !== null) { | |
platform = 'Chrome OS'; | |
platformVersion = padVersion(m[2]); | |
m2 = parseArch(m[1]); | |
architecture = m2[0]; | |
bitness = m2[1]; | |
} | |
if (!platform) { | |
platform = 'Unknown'; | |
} | |
// detect fullVersionList / brands | |
let notABrand = {brand: ' Not;A Brand', version: '99.0.0.0'}; | |
if ((m = /Chrome\/(\d+(\.\d+)*)/.exec(versionInfo)) !== null && navigator.vendor === 'Google Inc.') { | |
fullVersionList.push({brand: 'Chromium', version: padVersion(m[1], 4)}); | |
if ((m2 = /(Edge?)\/(\d+(\.\d+)*)/.exec(versionInfo)) !== null) { | |
let identBrandMap = { | |
'Edge': 'Microsoft Edge', | |
'Edg': 'Microsoft Edge', | |
}; | |
let brand = identBrandMap[m[1]]; | |
fullVersionList.push({brand: brand, version: padVersion(m2[2], 4)}); | |
} else { | |
fullVersionList.push({brand: 'Google Chrome', version: padVersion(m[1], 4)}); | |
} | |
if (/\bwv\b/.exec(platformInfo)) { | |
webview = true; | |
} | |
} else if ((m = /AppleWebKit\/(\d+(\.\d+)*)/.exec(versionInfo)) !== null && navigator.vendor === 'Apple Computer, Inc.') { | |
fullVersionList.push({brand: 'WebKit', version: padVersion(m[1])}); | |
if (platform === 'iOS' && (m2 = /(CriOS|EdgiOS|FxiOS|Version)\/(\d+(\.\d+)*)/.exec(versionInfo)) != null) { | |
let identBrandMap = { // no | |
'CriOS': 'Google Chrome', | |
'EdgiOS': 'Microsoft Edge', | |
'FxiOS': 'Mozilla Firefox', | |
'Version': 'Apple Safari', | |
}; | |
let brand = identBrandMap[m2[1]]; | |
fullVersionList.push({brand, version: padVersion(m2[2])}); | |
if (items.findIndex((s) => s.startsWith('Safari/')) === -1) { | |
webview = true; | |
} | |
} | |
} else if ((m = /Firefox\/(\d+(\.\d+)*)/.exec(versionInfo)) !== null) { | |
fullVersionList.push({brand: 'Firefox', version: padVersion(m[1])}); | |
} else { | |
fullVersionList.push(notABrand); | |
} | |
uaFullVersion = fullVersionList.length > 0 ? fullVersionList[fullVersionList.length - 1] : ''; | |
let brands = fullVersionList.map((b) => { | |
let pos = b.version.indexOf('.'); | |
let version = pos === -1 ? b.version : b.version.slice(0, pos); | |
return {brand: b.brand, version}; | |
}); | |
// TODO detect architecture, bitness and model | |
return { | |
mobile, | |
platform, | |
brands, | |
platformVersion, | |
architecture, | |
bitness, | |
model, | |
uaFullVersion, | |
fullVersionList, | |
webview | |
}; | |
} | |
function parseArch(arch) { | |
switch (arch) { | |
case 'x86_64': | |
case 'x64': | |
return ['x86', '64']; | |
case 'x86_32': | |
case 'x86': | |
return ['x86', '']; | |
case 'armv6l': | |
case 'armv7l': | |
case 'armv8l': | |
return [arch, '']; | |
case 'aarch64': | |
return ['arm', '64']; | |
default: | |
return ['', '']; | |
} | |
} | |
function padVersion(ver, minSegs = 3) { | |
let parts = ver.split('.'); | |
let len = parts.length; | |
if (len < minSegs) { | |
for (let i = 0, lenToPad = minSegs - len; i < lenToPad; i += 1) { | |
parts.push('0'); | |
} | |
return parts.join('.'); | |
} | |
return ver; | |
} | |
class NavigatorUAData { | |
constructor() { | |
this._ch = getClientHints(navigator); | |
Object.defineProperties(this, { | |
_ch: {enumerable: false}, | |
}); | |
} | |
get mobile() { | |
return this._ch.mobile; | |
} | |
get platform() { | |
return this._ch.platform; | |
} | |
get brands() { | |
return this._ch.brands; | |
} | |
getHighEntropyValues(hints) { | |
return new Promise((resolve, reject) => { | |
if (!Array.isArray(hints)) { | |
throw new TypeError('argument hints is not an array'); | |
} | |
let hintSet = new Set(hints); | |
let data = this._ch; | |
let obj = { | |
mobile: data.mobile, | |
platform: data.platform, | |
brands: data.brands, | |
}; | |
if (hintSet.has('architecture')) | |
obj.architecture = data.architecture; | |
if (hintSet.has('bitness')) | |
obj.bitness = data.bitness; | |
if (hintSet.has('model')) | |
obj.model = data.model; | |
if (hintSet.has('platformVersion')) | |
obj.platformVersion = data.platformVersion; | |
if (hintSet.has('uaFullVersion')) | |
obj.uaFullVersion = data.uaFullVersion; | |
if (hintSet.has('fullVersionList')) | |
obj.fullVersionList = data.fullVersionList; | |
resolve(obj); | |
}); | |
} | |
toJSON() { | |
let data = this._ch; | |
return { | |
mobile: data.mobile, | |
brands: data.brands, | |
}; | |
} | |
} | |
Object.defineProperty(NavigatorUAData.prototype, Symbol.toStringTag, { | |
enumerable: false, | |
configurable: true, | |
writable: false, | |
value: 'NavigatorUAData' | |
}); | |
function ponyfill() { | |
return new NavigatorUAData(navigator); | |
} | |
function polyfill() { | |
if (location.protocol === 'https:' && !navigator.userAgentData) { | |
let userAgentData = new NavigatorUAData(navigator); | |
Object.defineProperty(Navigator.prototype, 'userAgentData', { | |
enumerable: true, | |
configurable: true, | |
get: function getUseAgentData() { | |
return userAgentData; | |
} | |
}); | |
Object.defineProperty(window, 'NavigatorUAData', { | |
enumerable: false, | |
configurable: true, | |
writable: true, | |
value: NavigatorUAData | |
}); | |
return true; | |
} | |
return false; | |
} | |
export {ponyfill, polyfill}; |
Join our newsletter to stay up to date on new releases.
By subscribing you agree to our Privacy Policy and provide email consent