Browser Detection Using The User Agent

A modern guide to detecting browsers in JavaScript, covering feature detection, client hints, and why UA parsing creates fragile code.

Modern web development demands sophisticated approaches to cross-browser compatibility. While detecting a user's browser through the user agent string remains technically possible, the landscape has evolved significantly. This guide explores how to properly handle browser detection in 2025, emphasizing feature detection over browser identification and understanding the privacy-driven changes to user agent strings.

What You'll Learn

  • How user agent strings work and why they exist
  • Why UA sniffing leads to fragile, buggy code
  • Modern alternatives that produce more reliable results
  • Practical JavaScript patterns for cross-browser development
  • The role of client hints in privacy-conscious browser detection

Understanding the User Agent String

The user agent (UA) string is a header that browsers send with every HTTP request, identifying themselves to web servers. In JavaScript, you can access this string through the navigator.userAgent property. Understanding its structure is essential before attempting any detection logic.

Structure of a Modern User Agent String

A typical user agent string contains several components that identify the browser, version, and operating system. The string follows a format that has evolved over the years but maintains certain recognizable patterns. Modern browsers like Chrome, Firefox, Safari, and Edge all include specific identifiers that distinguish them from one another.

The string begins with product tokens and version numbers, followed by platform information and additional details about the browser's rendering engine. Chrome browsers, for instance, include "AppleWebKit" and "KHTML, like Gecko" references due to their WebKit heritage, while also including "Chrome" to identify the actual browser. This dual-reference approach historically allowed browsers to access both Chrome-specific and Safari-optimized content.

Modern browsers have adopted privacy-preserving measures that reduce the information available in these strings. User-Agent Reduction, now widely implemented across major browsers, limits the specificity of version numbers and platform details. The major version remains visible, but minor version numbers are replaced with zeros, and platform versions are generalized to broader categories like "Android 10" instead of specific device models.

Accessing the User Agent in JavaScript

JavaScript provides straightforward access to the user agent string through the Navigator API. The navigator.userAgent property returns the complete UA string for the current browser context. This property has been available since the earliest versions of JavaScript and remains supported across all modern browsers, though its reliability for detection purposes has diminished.

// Basic user agent access
const userAgent = navigator.userAgent;
console.log(userAgent);

// Example output for Chrome on Windows:
// Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36

// Example output for Safari on macOS:
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15

// Example output for Firefox on Linux:
// Mozilla/5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0

The Navigator API also provides related properties that offer partial information. The navigator.appVersion property returns similar information but without the explicit browser name in some cases. The navigator.platform property indicates the operating system platform, though this has also been affected by privacy reductions and may return generalized values.

Parsing the String Components

When you must parse a user agent string, understanding the common patterns helps avoid detection errors. The string contains space-separated tokens, with product names followed by version numbers in parentheses for platform-specific information. Parentheses typically enclose platform details, while additional comments follow version numbers.

Modern UA strings follow a structured format where product identifiers appear first, followed by version information. The platform section within parentheses provides operating system details. Browser-specific identifiers typically appear as additional tokens or within comments. Recognizing these patterns helps create more accurate parsing logic, though the complexity itself argues for avoiding UA parsing when possible.

To understand how these browser communications work at a deeper level, see our guide on the evolution of HTTP protocols that underpin web communication.

Code Example: Basic UA Access

// Capture the user agent string
function getUserAgent() {
 return navigator.userAgent || 'Not available';
}

// Extract browser name using regex patterns
function detectBrowserFromUA(ua) {
 let browser = 'Unknown';
 
 if (ua.includes('Chrome')) {
 browser = 'Chrome';
 } else if (ua.includes('Safari') && !ua.includes('Chrome')) {
 browser = 'Safari';
 } else if (ua.includes('Firefox')) {
 browser = 'Firefox';
 } else if (ua.includes('Edg')) {
 browser = 'Edge';
 }
 
 return browser;
}

// Detect operating system
function detectOS(ua) {
 let os = 'Unknown';
 
 if (ua.includes('Windows')) {
 os = 'Windows';
 } else if (ua.includes('Mac OS')) {
 os = 'macOS';
 } else if (ua.includes('Linux')) {
 os = 'Linux';
 } else if (ua.includes('Android')) {
 os = 'Android';
 } else if (ua.includes('iOS') || ua.includes('iPhone') || ua.includes('iPad')) {
 os = 'iOS';
 }
 
 return os;
}

// Log browser information
console.log('Browser:', detectBrowserFromUA(navigator.userAgent));
console.log('OS:', detectOS(navigator.userAgent));

Why UA Sniffing Is Problematic

Despite its apparent simplicity, browser detection through user agent parsing introduces significant maintenance burdens and reliability issues. The fundamental problem lies in treating browser identity as a proxy for feature support, when the two concepts are fundamentally different.

The Feature Detection Principle

The core issue with UA sniffing is that it answers the wrong question. When developers write browser detection code, they typically want to know whether a particular feature works, not which browser is rendering the page. These are completely different questions, and the mapping from browser to feature support is unreliable, constantly changing, and impossible to maintain comprehensively.

Consider a common example: detecting whether a browser supports a modern JavaScript feature. Using UA sniffing, a developer might check for Chrome version 90 or higher to enable modern syntax features. This approach fails in multiple ways. Chrome versions below 90 might have received feature updates through automatic updates. Other browsers like Edge, Opera, or even other Chromium-based browsers might support the same features but fail the Chrome-specific check. Firefox or Safari might have implemented the feature in earlier versions, making the Chrome version requirement arbitrary.

Feature detection solves these problems by testing for the actual capability rather than inferring it from browser identity. If you need to know whether a browser supports a particular API, test for that API's existence directly. This approach works regardless of which browser provides the support, adapts to automatic updates automatically, and requires no maintenance as browsers evolve. For building robust web applications that work across all browsers, learn more about our web development services.

Maintenance and Reliability Issues

UA sniffing code accumulates technical debt rapidly. Every new browser release potentially requires updates to detection logic. Browser vendors periodically change their UA strings, which can break detection code that relies on specific formatting. New browsers enter the market with unfamiliar UA patterns. Privacy-focused browsers modify their strings to avoid fingerprinting. These factors combine to make UA detection a moving target.

The maintenance burden compounds over time. Code written three years ago may have detection logic that worked perfectly but now fails due to browser changes. Teams must allocate developer time to monitor browser releases, update detection logic, and debug issues caused by outdated checks. This overhead directly competes with feature development and user experience improvements.

Reliability suffers from the complexity of maintaining comprehensive detection logic. Edge cases proliferate as different browsers, versions, and platforms interact. A check that works for Chrome on Windows might fail for Chrome on macOS. Detection for the current version might break when the browser auto-updates. Testing becomes exponentially complex as you attempt to verify behavior across all combinations.

Privacy Concerns and Browser Responses

Browsers have increasingly recognized user agent parsing as a privacy concern. The extensive information in UA strings enables fingerprinting, where trackers combine UA details with other signals to identify users across sessions without cookies. This recognition drove the User-Agent Reduction initiative, implemented across Chrome, Firefox, Safari, and Edge.

User-Agent Reduction limits the information available in UA strings. Platform versions are generalized to broad categories like "Android 10" instead of specific device models. Minor browser versions are replaced with zeros, showing only major version numbers. Device model information is removed entirely. These changes improve user privacy but further reduce the reliability of UA-based detection.

According to MDN Web Docs on User-Agent Reduction, the reduction program demonstrates that browsers view UA string parsing as an anti-pattern to be discouraged rather than a supported feature to be maintained. Relying on UA detection now means building on ground that browsers are actively making less stable. Feature detection and client hints provide more sustainable alternatives that align with browser vendor recommendations.

Code Example: Why Feature Detection Works Better

// UNRELIABLE: UA sniffing approach
function enableModernFeatures_UA() {
 const ua = navigator.userAgent;
 const isChrome = ua.includes('Chrome/');
 const versionMatch = ua.match(/Chrome\/(\d+)/);
 const version = versionMatch ? parseInt(versionMatch[1]) : 0;
 
 if (isChrome && version >= 90) {
 // Enable modern features
 return true;
 }
 return false;
}

// RELIABLE: Feature detection approach
function enableModernFeatures_Feature() {
 // Test for actual capabilities, not browser identity
 return typeof BigInt !== 'undefined' &&
 typeof Symbol !== 'undefined' &&
 typeof Promise !== 'undefined';
}

// Practical example: detecting ResizeObserver
function setupResponsiveComponents_UA() {
 // Fragile - depends on specific browser versions
 const isModernBrowser = navigator.userAgent.includes('Chrome/9');
 if (isModernBrowser) {
 // Use ResizeObserver
 return true;
 }
 return false;
}

function setupResponsiveComponents_Feature() {
 // Robust - tests actual capability
 if ('ResizeObserver' in window) {
 const observer = new ResizeObserver(entries => {
 entries.forEach(entry => {
 console.log('Element resized:', entry.contentRect);
 });
 });
 observer.observe(document.body);
 return true;
 }
 // Fallback to less efficient method
 window.addEventListener('resize', handleResize);
 return false;
}

Feature Detection: The Recommended Approach

Feature detection represents the proper solution to cross-browser compatibility challenges. Instead of asking "which browser is this?", feature detection asks "does this capability exist?". This question produces more reliable, maintainable code that adapts automatically to browser updates.

Testing for JavaScript APIs

Modern JavaScript provides multiple patterns for feature detection. The most straightforward approach checks for the existence of objects, properties, or functions before using them. This pattern works because undefined properties indicate missing implementations, while defined properties indicate available features.

// Detect API availability before using it
if ('geolocation' in navigator) {
 navigator.geolocation.getCurrentPosition(
 position => console.log('Location:', position.coords),
 error => console.error('Geolocation error:', error)
 );
} else {
 console.log('Geolocation not available - using fallback');
}

// Detect specific object properties
if (typeof window.ResizeObserver !== 'undefined') {
 const observer = new ResizeObserver(entries => {
 for (const entry of entries) {
 console.log('Element resized:', entry.contentRect);
 }
 });
 observer.observe(document.body);
} else {
 window.addEventListener('resize', () => {
 console.log('Window resized - less efficient fallback');
 });
}

// Detect modern JavaScript features
if (typeof BigInt !== 'undefined') {
 const largeNumber = BigInt(9007199254740991);
 console.log('BigInt supported:', largeNumber);
}

// Detect WebAssembly support
if (typeof WebAssembly !== 'undefined' && typeof WebAssembly.instantiate === 'function') {
 console.log('WebAssembly supported');
}

This pattern extends to detecting entire API surfaces rather than individual methods. Checking for the existence of an object often suffices when you need multiple related features from that API. The browser either implements an API completely or not at all in most cases, making coarse-grained detection appropriate.

CSS Feature Detection with @supports

CSS provides its own feature detection mechanism through the @supports at-rule. This rule tests for CSS property-value combinations, enabling progressive enhancement in stylesheets without JavaScript. The rule supports logical operators for complex conditions and negative tests for fallback styling.

/* Detect grid layout support */
@supports (display: grid) {
 .container {
 display: grid;
 grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
 gap: 1rem;
 }
}

/* Detect custom property support */
@supports (--custom-property: value) {
 :root {
 --primary-color: #0066cc;
 }
}

/* Detect specific value support */
@supports (backdrop-filter: blur(10px)) {
 .glass-effect {
 background: rgba(255, 255, 255, 0.2);
 backdrop-filter: blur(10px);
 }
}

/* Negative test for fallback */
@supports not (container-type: inline-size) {
 .card-container {
 display: block;
 }
}

/* Combined conditions */
@supports (display: flex) and (gap: 1rem) {
 .flex-layout {
 display: flex;
 gap: 1rem;
 }
}

The @supports rule integrates naturally with responsive design workflows. Rather than detecting screen sizes to determine layout capabilities, test for the layout features themselves. A grid-capable browser gets grid layouts regardless of screen size; older browsers receive alternative layouts that work within their capabilities.

Progressive Enhancement Patterns

Progressive enhancement builds upon feature detection to create experiences that work everywhere while enhancing for capable browsers. The pattern layers functionality from baseline capabilities through increasingly sophisticated features, with each layer building upon the previous rather than replacing it. This approach is fundamental to modern web applications, similar to the principles behind progressive web apps that deliver enhanced experiences on capable devices while maintaining core functionality everywhere.

// Progressive enhancement example: Image lazy loading
function setupLazyLoading() {
 const images = document.querySelectorAll('img[data-src]');
 
 // Test for IntersectionObserver
 if ('IntersectionObserver' in window) {
 const observer = new IntersectionObserver((entries, obs) => {
 entries.forEach(entry => {
 if (entry.isIntersecting) {
 const img = entry.target;
 img.src = img.dataset.src;
 img.removeAttribute('data-src');
 obs.unobserve(img);
 }
 });
 }, {
 rootMargin: '50px 0px',
 threshold: 0.01
 });
 
 images.forEach(img => observer.observe(img));
 } else {
 // Fallback: load all images immediately
 images.forEach(img => {
 img.src = img.dataset.src;
 img.removeAttribute('data-src');
 });
 }
}

The progressive enhancement mindset shifts focus from supporting specific browsers to supporting specific capabilities. This perspective produces more maintainable code because capability detection rarely requires updates, while browser detection does. When a browser gains a capability, enhanced code paths activate automatically without code changes.

Code Examples

// JavaScript API detection patterns
function detectAPIs() {
 return {
 geolocation: 'geolocation' in navigator,
 serviceWorker: 'serviceWorker' in navigator,
 pushManager: 'PushManager' in window,
 broadcastChannel: 'BroadcastChannel' in window,
 webSocket: 'WebSocket' in window,
 indexedDB: 'indexedDB' in window,
 cookieEnabled: navigator.cookieEnabled
 };
}

// CSS @supports usage
function supportsCSSFeature(property, value) {
 if (typeof CSS !== 'undefined' && CSS.supports) {
 return CSS.supports(property, value);
 }
 return false;
}

// Check multiple features
console.log('Grid supported:', supportsCSSFeature('display', 'grid'));
console.log('Container queries:', supportsCSSFeature('container-type', 'inline-size'));

// Progressive enhancement for image lazy loading
if ('loading' in HTMLImageElement.prototype) {
 console.log('Native lazy loading supported');
} else {
 // Dynamically load a lazy loading polyfill
 const script = document.createElement('script');
 script.src = '/js/lazy-load-polyfill.js';
 document.head.appendChild(script);
}

Mobile Device Detection

A common use case for browser detection involves determining whether users are on mobile devices. However, the goal typically isn't detecting "mobile" per se but rather adapting to touch interfaces, smaller screens, or limited device capabilities. Modern APIs provide more reliable approaches than UA parsing.

Touch Capability Detection

Modern devices report touch capability through the Navigator API, providing more reliable detection than parsing mobile indicators from UA strings. The maxTouchPoints property indicates the maximum number of simultaneous touch points the device supports, with values greater than zero indicating touch capability.

// Detect touch capability
function isTouchDevice() {
 return (navigator.maxTouchPoints > 0) ||
 ('ontouchstart' in window) ||
 (navigator.msMaxTouchPoints > 0);
}

// Practical application: adjust UI for touch
function setupResponsiveUI() {
 const isTouch = navigator.maxTouchPoints > 0;
 
 if (isTouch) {
 document.documentElement.classList.add('touch-device');
 
 const buttons = document.querySelectorAll('button, .btn');
 buttons.forEach(btn => {
 btn.style.minHeight = '44px';
 btn.style.padding = '12px 24px';
 });
 }
}

// Detect high-precision pointing (mouse vs touch)
function getPointingType() {
 if (window.matchMedia('(pointer: coarse)').matches) {
 return 'touch';
 } else if (window.matchMedia('(pointer: fine)').matches) {
 return 'mouse';
 } else {
 return 'unknown';
 }
}

The touch detection approach works across modern browsers and devices. Tablets with mouse input report different capabilities than phones, which may be desirable depending on your use case. The detection is more stable than UA parsing because touch capability changes rarely, while UA strings change with every browser update.

Screen and Viewport Detection

Media queries provide reliable information about screen characteristics without UA parsing. The window.matchMedia API tests for specific conditions, while window.screen properties reveal display dimensions. These approaches work regardless of how browsers format their UA strings.

// Detect screen orientation
function getOrientation() {
 if (window.matchMedia('(orientation: portrait)').matches) {
 return 'portrait';
 } else if (window.matchMedia('(orientation: landscape)').matches) {
 return 'landscape';
 }
 return 'unknown';
}

// Detect actual screen size vs viewport
function getScreenInfo() {
 return {
 width: window.screen.width,
 height: window.screen.height,
 availWidth: window.screen.availWidth,
 availHeight: window.screen.availHeight,
 colorDepth: window.screen.colorDepth,
 pixelRatio: window.devicePixelRatio
 };
}

// Detect reduced motion preference (accessibility)
function respectsReducedMotion() {
 return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}

// Detect dark mode preference
function prefersDarkMode() {
 return window.matchMedia('(prefers-color-scheme: dark)').matches;
}

// Detect data saver preference
function isDataSaverEnabled() {
 const connection = navigator.connection ||
 navigator.mozConnection ||
 navigator.webkitConnection;
 return connection ? connection.saveData : false;
}

Media query detection adapts to device changes like rotation or window resizing. You can register listeners for media query changes to update UI dynamically as conditions change. This reactive approach provides better user experiences than static UA-based detection that only runs on page load.

Code Examples

// Touch device detection
const detectTouchDevice = () => {
 const touchPoints = navigator.maxTouchPoints || 0;
 const touchStart = 'ontouchstart' in window;
 const msTouchPoints = navigator.msMaxTouchPoints || 0;
 return touchPoints > 0 || touchStart || msTouchPoints > 0;
};

// Pointing type detection (mouse vs touch)
function detectPointingType() {
 if (window.matchMedia('(pointer: coarse)').matches) {
 return 'finger';
 }
 if (window.matchMedia('(pointer: fine)').matches) {
 return 'mouse';
 }
 if (window.matchMedia('(pointer: none)').matches) {
 return 'keyboard';
 }
 return 'unknown';
}

// Screen information retrieval with media query listeners
function setupScreenDetection() {
 const screenInfo = getScreenInfo();
 console.log('Screen:', screenInfo);
 
 // Listen for orientation changes
 window.matchMedia('(orientation: portrait)')
 .addEventListener('change', e => {
 console.log('Orientation changed to:', e.matches ? 'portrait' : 'landscape');
 });
 
 // Listen for dark mode changes
 window.matchMedia('(prefers-color-scheme: dark)')
 .addEventListener('change', e => {
 console.log('Dark mode:', e.matches);
 document.documentElement.classList.toggle('dark', e.matches);
 });
}

// Media query based adaptations
function applyMediaQueryAdaptations() {
 // Apply touch-specific styles
 if (window.matchMedia('(pointer: coarse)').matches) {
 document.body.classList.add('touch-optimized');
 }
 
 // Apply reduced motion
 if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
 document.body.classList.add('reduce-motion');
 }
}

Client Hints: The Modern Alternative

Client hints provide a privacy-preserving alternative to UA parsing for cases where browser information genuinely improves user experience. Rather than including detailed browser information in every request, client hints require servers to explicitly request specific information, giving users visibility and control over what data they share.

Understanding the Client Hints API

Client hints arrive through HTTP headers that browsers send in response to server requests. The Accept-CH header indicates which client hints a server wants to receive. Browsers then include those hints in subsequent requests. This opt-in model differs fundamentally from UA strings, which send comprehensive information by default.

# Server requests specific client hints
Accept-CH: Sec-CH-UA, Sec-CH-UA-Mobile, Sec-CH-UA-Platform, Sec-CH-UA-Arch

# Client response includes requested hints
Sec-CH-UA: "Chrome";v="131", "Not_A Brand";v="8"
Sec-CH-UA-Mobile: ?0
Sec-CH-UA-Platform: "Windows"
Sec-CH-UA-Arch: "x86"

The JavaScript API provides equivalent access to client hints through the navigator.userAgentData property. This property returns a NavigatorUAData object with brand and platform information. High-entropy hints require explicit permission requests, further protecting user privacy.

Accessing Client Hints in JavaScript

// Access low-entropy client hints (readily available)
if (navigator.userAgentData) {
 const brands = navigator.userAgentData.brands;
 const mobile = navigator.userAgentData.mobile;
 const platform = navigator.userAgentData.platform;
 
 console.log('Browser brands:', brands);
 console.log('Is mobile:', mobile);
 console.log('Platform:', platform);
}

// Request high-entropy hints (requires permission)
async function getDetailedClientInfo() {
 if (navigator.userAgentData &&
 navigator.userAgentData.getHighEntropyValues) {
 try {
 const hints = await navigator.userAgentData.getHighEntropyValues([
 'architecture',
 'model',
 'platformVersion',
 'fullVersionList'
 ]);
 console.log('Detailed hints:', hints);
 return hints;
 } catch (error) {
 console.log('High-entropy hints not available:', error);
 return null;
 }
 }
 return null;
}

The distinction between low-entropy and high-entropy hints reflects the privacy model. Low-entropy hints like brand names and platform categories reveal little individually but combine to identify users. High-entropy hints like specific model numbers or full version lists provide fingerprinting opportunities, hence requiring explicit permission.

When to Use Client Hints

Client hints serve legitimate purposes when servers need browser information to optimize responses. Content delivery networks use hints to serve appropriately sized images. Analytics platforms use hints to aggregate browser statistics. Development tools use hints to provide relevant debugging information.

According to MDN Web Docs on Client Hints, however, client hints should not replace feature detection for application logic. If you need to know whether a browser supports a feature, test for that feature directly rather than inferring support from client hints. Client hints provide environmental context; feature detection provides capability information. Use each for its intended purpose.

Code Examples

// HTTP header examples for requesting hints (server-side)
// In your server configuration, add:
// Accept-CH: Sec-CH-UA, Sec-CH-UA-Mobile, Sec-CH-UA-Platform
// Critical-CH: Sec-CH-UA-Mobile

// JavaScript API for accessing hints
async function getClientHints() {
 if (!navigator.userAgentData) {
 console.log('Client hints not supported');
 return null;
 }
 
 const hints = {
 brands: navigator.userAgentData.brands,
 mobile: navigator.userAgentData.mobile,
 platform: navigator.userAgentData.platform
 };
 
 // Try to get high-entropy values
 if (navigator.userAgentData.getHighEntropyValues) {
 try {
 const detailed = await navigator.userAgentData.getHighEntropyValues([
 'platformVersion',
 'architecture'
 ]);
 hints.detailed = detailed;
 } catch (e) {
 console.log('High-entropy hints denied or unavailable');
 }
 }
 
 return hints;
}

// Practical use: adapt content based on hints
async function adaptContentWithHints() {
 const hints = await getClientHints();
 
 if (!hints) {
 // Fallback to default content
 return 'default-content';
 }
 
 // Check if on mobile for responsive images
 if (hints.mobile) {
 return 'mobile-optimized-content';
 }
 
 return 'desktop-content';
}

Practical JavaScript Patterns

Building on the principles above, these practical patterns address common detection scenarios while avoiding UA sniffing pitfalls. Each pattern demonstrates proper capability detection or appropriate client hints usage for specific use cases.

Modern JavaScript Feature Detection

// Detect ES6+ features
const detectES6Support = () => {
 const features = {
 arrowFunctions: typeof Symbol !== 'undefined' &&
 typeof (() => {}) === 'function',
 constLet: (() => {
 try {
 eval('const x = 1');
 return true;
 } catch (e) {
 return false;
 }
 })(),
 classes: typeof class {} === 'function',
 promises: typeof Promise !== 'undefined',
 asyncAwait: (() => {
 try {
 eval('(async function(){})()');
 return true;
 } catch (e) {
 return false;
 }
 })(),
 modules: typeof Symbol !== 'undefined' &&
 typeof Symbol.asyncIterator !== 'undefined',
 bigInt: typeof BigInt !== 'undefined',
 dynamicImport: typeof import === 'function'
 };
 
 return features;
};

// Use detected features to adapt code loading
function loadAppropriateModule() {
 const features = detectES6Support();
 
 if (features.asyncAwait && features.modules) {
 import('./modern-module.js')
 .then(module => module.init())
 .catch(err => console.error('Module load failed:', err));
 } else if (features.promises) {
 require.ensure = require.ensure || (() => Promise.resolve());
 require(['./legacy-module.js'], module => module.init());
 } else {
 require('./legacy-module.js').init();
 }
}

DOM API Detection

// Comprehensive DOM feature detection
const domFeatures = {
 querySelector: typeof document.querySelector === 'function',
 querySelectorAll: typeof document.querySelectorAll === 'function',
 closest: typeof Element.prototype.closest === 'function',
 matches: typeof Element.prototype.matches === 'function' ||
 typeof Element.prototype.webkitMatchesSelector === 'function',
 mutationObserver: typeof MutationObserver !== 'undefined',
 resizeObserver: typeof ResizeObserver !== 'undefined',
 intersectionObserver: typeof IntersectionObserver !== 'undefined',
 webAnimations: typeof Element.prototype.animate === 'function',
 templateElement: typeof HTMLTemplateElement !== 'undefined',
 shadowDOM: typeof Element.prototype.attachShadow === 'function',
 customElements: typeof customElements !== 'undefined' &&
 typeof customElements.define === 'function',
 containerQueries: typeof CSSContainerRule !== 'undefined'
};

// Practical use: create elements with appropriate methods
function createCardElement() {
 if (domFeatures.shadowDOM) {
 const card = document.createElement('div');
 const shadow = card.attachShadow({ mode: 'open' });
 shadow.innerHTML = `
 <style>
 :host { display: block; }
 .content { padding: 16px; }
 </style>
 <div class="content">
 <slot></slot>
 </div>
 `;
 return card;
 } else {
 const card = document.createElement('div');
 card.className = 'card-fallback';
 card.innerHTML = '<div class="content"><slot></slot></div>';
 return card;
 }
}

Network and Storage Detection

// Detect storage capabilities
const storageCapabilities = () => {
 const capabilities = {
 localStorage: false,
 sessionStorage: false,
 indexedDB: false,
 cacheAPI: false
 };
 
 try {
 capabilities.localStorage = typeof localStorage !== 'undefined';
 capabilities.sessionStorage = typeof sessionStorage !== 'undefined';
 } catch (e) {
 // Storage access blocked
 }
 
 try {
 capabilities.indexedDB = typeof indexedDB !== 'undefined';
 } catch (e) {
 // IndexedDB not available
 }
 
 try {
 capabilities.cacheAPI = 'caches' in window;
 } catch (e) {
 // Cache API not available
 }
 
 return capabilities;
};

// Detect connection type
async function getNetworkInfo() {
 const connection = navigator.connection ||
 navigator.mozConnection ||
 navigator.webkitConnection;
 
 if (connection) {
 return {
 effectiveType: connection.effectiveType,
 downlink: connection.downlink,
 rtt: connection.rtt,
 saveData: connection.saveData
 };
 }
 
 return null;
}

// Adaptive content based on network conditions
async function loadResourcesAppropriately() {
 const network = await getNetworkInfo();
 const storage = storageCapabilities();
 
 if (network && (network.effectiveType === '2g' || network.effectiveType === 'slow-2g')) {
 loadCriticalCSSOnly();
 deferNonEssentialJavaScript();
 requestImageLazyLoading();
 } else if (network && network.saveData) {
 skipHighResImages();
 disableAutoPlayVideos();
 } else {
 loadAllResources();
 }
}

Frequently Asked Questions

Conclusion and Best Practices

Browser detection through user agent parsing represents an outdated approach that creates maintenance burdens and reliability issues. Modern web development requires shifting from "which browser?" to "what capabilities?" thinking. Feature detection produces more reliable code that adapts automatically to browser updates without manual maintenance.

When browser information genuinely improves user experience, client hints provide a privacy-preserving alternative that respects user autonomy. The opt-in model of client hints ensures users understand what information they share, addressing privacy concerns that drove the user-agent reduction initiative.

Key Takeaways

  1. Avoid UA parsing for application logic - it creates maintenance burden and reliability issues
  2. Use feature detection to test for actual capabilities rather than inferring them from browser identity
  3. Employ progressive enhancement to layer functionality from baseline to enhanced experiences
  4. Consider client hints for legitimate server-side optimization needs
  5. Test capabilities directly rather than maintaining comprehensive browser support matrices

By adopting these practices, developers create more maintainable applications that automatically benefit from browser improvements without requiring code updates. The initial investment in capability detection patterns pays dividends through reduced maintenance costs and improved reliability across the evolving browser landscape.

Our web development services team specializes in building robust, cross-browser compatible applications using these modern best practices. Whether you're starting a new project or need to refactor legacy detection code, we can help ensure your applications work reliably across all browsers and devices.

Build Cross-Browser Compatible Web Applications

Our team of experienced JavaScript developers creates robust web applications that work reliably across all browsers and devices using modern best practices.