JavaScript - Browser Detection Utility

9 min read

This JavaScript utility helps your website identify and adapt to different browsers and devices. Here’s what it does in simple terms:

🔍 What This Code Does

When someone visits your website, this script:

  1. Identifies their browser (Chrome, Firefox, Safari, Edge, Opera)

  2. Detects their device type (desktop, tablet, or mobile)

  3. Recognizes their operating system (Windows, macOS, iOS, Android, Linux)

  4. Checks screen size and adjusts when the window resizes

  5. Detects touch capabilities and other browser features

🛠️ How It Works

The script adds helpful CSS classes to your webpage’s <body> element, such as:

  • chrome-browser or firefox-browser

  • mobile-device or tablet-device`

  • ios-platform or windows-platform

  • touch-device (for touchscreens)

  • viewport-mobile or viewport-desktop (for screen sizes)

💡 Why It’s Useful

These classes let you:

  • Create browser-specific styles when needed

  • Fix compatibility issues for certain browsers

  • Enhance mobile experiences with targeted CSS

  • Adapt layouts based on device capabilities

  • Implement responsive design with JavaScript support

📝 How To Use The Classes

Once the script is running, you can write CSS like this:

/* Make buttons larger on touch devices */
.touch-device .button {
	padding: 12px 24px;
	font-size: 18px;
}

/* Fix a Safari-specific issue */
.safari-browser .problem-element {
	transform: translateZ(0);
}

/* Adjust layout for mobile */
.mobile-device .sidebar {
	display: none;
}
/* Fix for Safari flexbox gap issues */
.safari-browser .card-grid {
	gap: 0;
}

.safari-browser .card-grid > .card {
	margin: 10px;
}

Here’s the complete browser detection utility:

/**
 * Enhanced Browser Detection Utility
 *
 * This script identifies the user's browser and applies appropriate classes to the document body.
 * It also provides responsive class handling for different screen sizes.
 *
 * Features:
 * - Detects major browsers (Safari, Firefox, Chrome, Edge, Opera)
 * - Detects mobile browsers and applies appropriate classes
 * - Applies browser-specific classes to body element
 * - Adds responsive classes for different screen sizes
 * - Provides feature detection for more reliable browser identification
 * - Handles edge cases and errors gracefully
 * - Uses a configurable, efficient event handling system
 * - Includes memory management to prevent leaks
 */
document.addEventListener("DOMContentLoaded", function () {
	const BrowserDetector = {
		/**
		 * Configuration options with defaults
		 */
		config: {
			resizeDebounceTime: 200,
			breakpoints: {
				mobile: 480,
				tablet: 768,
				desktop: 1025,
				largeDesktop: 1440,
			},
			enableLogging: false,
			enableFeatureDetection: true,
			fallbackBrowserClass: "unknown-browser",
		},

		/**
		 * Current detected browser and platform state
		 */
		state: {
			currentBrowser: null,
			isMobile: false,
			isTouch: false,
			isPrivate: false,
			isLegacy: false,
			initialized: false,
		},

		/**
		 * Browser detection methods
		 * Each returns boolean indicating if the browser matches
		 */
		browsers: {
			safari: function () {
				// Enhanced Safari detection
				const vendor = navigator.vendor || "";
				// Regular Safari
				const isSafari =
					vendor.includes("Apple") &&
					!navigator.userAgent.includes("Chrome") &&
					!navigator.userAgent.includes("Edg");

				// Safari on iOS
				const isSafariIOS =
					/^((?!chrome|android).)*safari/i.test(navigator.userAgent) ||
					(vendor.indexOf("Apple") > -1 &&
						navigator.userAgent.indexOf("CriOS") === -1 &&
						navigator.userAgent.indexOf("FxiOS") === -1);

				return isSafari || isSafariIOS;
			},
			firefox: function () {
				return navigator.userAgent.includes("Firefox") || navigator.userAgent.includes("FxiOS");
			},
			chrome: function () {
				const vendor = navigator.vendor || "";
				// Regular Chrome
				const isChrome =
					navigator.userAgent.includes("Chrome") &&
					vendor.includes("Google") &&
					!navigator.userAgent.includes("Edg");

				// Chrome on iOS
				const isChromeIOS = navigator.userAgent.includes("CriOS");

				return isChrome || isChromeIOS;
			},
			edge: function () {
				return navigator.userAgent.includes("Edg") || navigator.userAgent.includes("Edge");
			},
			opera: function () {
				return navigator.userAgent.includes("OPR") || navigator.userAgent.includes("Opera");
			},
			ie: function () {
				const isIE = /*@cc_on!@*/ false || !!document.documentMode;
				const isIECompatMode =
					navigator.userAgent.includes("Trident") || navigator.userAgent.includes("MSIE");
				return isIE || isIECompatMode;
			},
		},

		/**
		 * Feature detection methods for more reliable browser checks
		 */
		featureDetection: {
			isTouch: function () {
				return (
					"ontouchstart" in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0
				);
			},
			isWebkit: function () {
				try {
					return "WebkitAppearance" in document.documentElement.style;
				} catch (e) {
					return false;
				}
			},
			isGecko: function () {
				try {
					return "MozAppearance" in document.documentElement.style;
				} catch (e) {
					return false;
				}
			},
			isBlink: function () {
				// A simple test for Blink rendering engine
				return (
					(!!window.chrome || !!window.Intl) &&
					!!CSS &&
					"function" === typeof CSS.supports &&
					CSS.supports("(--foo: red)") &&
					!this.isGecko()
				);
			},
		},

		/**
		 * Platform detection methods
		 */
		platforms: {
			isMobilePlatform: function () {
				return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
					navigator.userAgent
				);
			},
			isTabletPlatform: function () {
				const userAgent = navigator.userAgent.toLowerCase();
				return /(ipad|tablet|playbook|silk)|(android(?!.*mobile))/i.test(userAgent);
			},
			isIOS: function () {
				return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
			},
			isAndroid: function () {
				return /Android/i.test(navigator.userAgent);
			},
			isMacOS: function () {
				return /Mac/.test(navigator.platform);
			},
			isWindows: function () {
				return /Win/.test(navigator.platform);
			},
			isLinux: function () {
				return /Linux/.test(navigator.platform);
			},
		},

		/**
		 * Advanced detection methods for edge cases
		 */
		advanced: {
			isLegacyBrowser: function () {
				// Check for IE or other legacy browsers
				const isIE = /*@cc_on!@*/ false || !!document.documentMode;
				const isOldEdge = /Edge\/\d./i.test(navigator.userAgent);
				const isOldFirefox =
					/Firefox\/(\d+)\./.test(navigator.userAgent) && parseInt(RegExp.$1, 10) < 40;
				const isOldChrome =
					/Chrome\/(\d+)\./.test(navigator.userAgent) && parseInt(RegExp.$1, 10) < 40;

				return isIE || isOldEdge || isOldFirefox || isOldChrome;
			},
			tryDetectPrivateMode: function () {
				// This is a best effort as private browsing detection is complex
				// and browsers actively work to prevent reliable detection
				try {
					const testKey = "browserDetectionTest";
					localStorage.setItem(testKey, "1");
					localStorage.removeItem(testKey);
					return false;
				} catch (e) {
					return true;
				}
			},
		},

		/**
		 * Utility methods for DOM and helper functions
		 */
		utils: {
			debounce: function (func, wait) {
				let timeout;
				return function (...args) {
					const context = this;
					clearTimeout(timeout);
					timeout = setTimeout(() => func.apply(context, args), wait);
				};
			},
			addClass: function (className) {
				if (className && typeof className === "string") {
					document.body.classList.add(className);
				}
			},
			removeClass: function (className) {
				if (className && typeof className === "string") {
					document.body.classList.remove(className);
				}
			},
			toggleClass: function (className, condition) {
				if (className && typeof className === "string") {
					document.body.classList.toggle(className, !!condition);
				}
			},
			log: function (message, data) {
				if (BrowserDetector.config.enableLogging) {
					console.log(`[BrowserDetector] ${message}`, data || "");
				}
			},
			error: function (message, error) {
				console.error(`[BrowserDetector] ${message}`, error || "");
			},
			cleanClassName: function (name) {
				return name.replace(/[^a-zA-Z0-9-_]/g, "").toLowerCase();
			},
		},

		/**
		 * Detect current browser and set appropriate classes
		 * Returns detected browser or 'unknown'
		 */
		detectBrowser: function () {
			try {
				// Remove previous browser classes
				document.body.className = document.body.className
					.split(" ")
					.filter((cls) => !cls.endsWith("-browser"))
					.join(" ");

				// Test each browser and apply when found
				for (const [browser, detectFn] of Object.entries(this.browsers)) {
					if (detectFn()) {
						const cleanName = this.utils.cleanClassName(browser);
						this.utils.addClass(`${cleanName}-browser`);
						this.state.currentBrowser = cleanName;
						this.utils.log(`Detected browser: ${cleanName}`);
						return cleanName;
					}
				}

				// Fallback for unknown browsers
				this.utils.addClass(this.config.fallbackBrowserClass);
				this.state.currentBrowser = "unknown";
				this.utils.log("Unknown browser detected");
				return "unknown";
			} catch (error) {
				this.utils.error("Error detecting browser", error);
				this.utils.addClass("detection-error");
				this.utils.addClass(this.config.fallbackBrowserClass);
				this.state.currentBrowser = "unknown";
				return "unknown";
			}
		},

		/**
		 * Detect platform features and set relevant classes
		 */
		detectPlatformFeatures: function () {
			try {
				// Detect and set mobile/tablet status
				this.state.isMobile = this.platforms.isMobilePlatform();
				this.state.isTablet = this.platforms.isTabletPlatform();
				this.utils.toggleClass("mobile-device", this.state.isMobile);
				this.utils.toggleClass("tablet-device", this.state.isTablet);

				// Detect and set OS status
				if (this.platforms.isIOS()) {
					this.utils.addClass("ios-platform");
				} else if (this.platforms.isAndroid()) {
					this.utils.addClass("android-platform");
				} else if (this.platforms.isMacOS()) {
					this.utils.addClass("macos-platform");
				} else if (this.platforms.isWindows()) {
					this.utils.addClass("windows-platform");
				} else if (this.platforms.isLinux()) {
					this.utils.addClass("linux-platform");
				}

				// Detect touch capability
				this.state.isTouch = this.featureDetection.isTouch();
				this.utils.toggleClass("touch-device", this.state.isTouch);

				// Legacy browser detection
				this.state.isLegacy = this.advanced.isLegacyBrowser();
				this.utils.toggleClass("legacy-browser", this.state.isLegacy);

				// Private browsing (best effort)
				if (this.config.detectPrivateBrowsing) {
					this.state.isPrivate = this.advanced.tryDetectPrivateMode();
					this.utils.toggleClass("private-browsing", this.state.isPrivate);
				}

				this.utils.log("Platform features detected", this.state);
			} catch (error) {
				this.utils.error("Error detecting platform features", error);
				this.utils.addClass("feature-detection-error");
			}
		},

		/**
		 * Handle responsive classes based on window size
		 */
		handleResponsive: function () {
			try {
				const width = window.innerWidth;
				const { breakpoints } = this.config;
				const currentBrowser = this.state.currentBrowser;

				if (!currentBrowser) {
					this.utils.error("Cannot set responsive classes: browser not detected");
					return;
				}

				// Remove previous responsive classes
				for (const size of Object.keys(breakpoints)) {
					this.utils.removeClass(`${currentBrowser}-${size}`);
				}

				// Add appropriate responsive class
				if (width < breakpoints.mobile) {
					this.utils.addClass(`${currentBrowser}-mobile`);
					this.utils.addClass("viewport-mobile");
				} else if (width < breakpoints.tablet) {
					this.utils.addClass(`${currentBrowser}-tablet`);
					this.utils.addClass("viewport-tablet");
				} else if (width < breakpoints.desktop) {
					this.utils.addClass(`${currentBrowser}-tablet-large`);
					this.utils.addClass("viewport-tablet-large");
				} else if (width < breakpoints.largeDesktop) {
					this.utils.addClass(`${currentBrowser}-desktop`);
					this.utils.addClass("viewport-desktop");
				} else {
					this.utils.addClass(`${currentBrowser}-desktop-large`);
					this.utils.addClass("viewport-desktop-large");
				}

				this.utils.log(`Responsive classes updated for width: ${width}px`);
			} catch (error) {
				this.utils.error("Error handling responsive classes", error);
				this.utils.addClass("responsive-error");
			}
		},

		/**
		 * Validate and normalize configuration
		 */
		validateConfig: function (customConfig) {
			try {
				// Merge custom config with defaults
				if (customConfig && typeof customConfig === "object") {
					this.config = { ...this.config, ...customConfig };
				}

				// Ensure breakpoints are in ascending order
				const { breakpoints } = this.config;
				if (
					breakpoints.mobile > breakpoints.tablet ||
					breakpoints.tablet > breakpoints.desktop ||
					breakpoints.desktop > breakpoints.largeDesktop
				) {
					this.utils.error("Invalid breakpoints configuration: values must be in ascending order");
					// Reset to defaults
					this.config.breakpoints = {
						mobile: 480,
						tablet: 768,
						desktop: 1025,
						largeDesktop: 1440,
					};
				}

				// Ensure debounce time is reasonable
				if (
					typeof this.config.resizeDebounceTime !== "number" ||
					this.config.resizeDebounceTime < 50 ||
					this.config.resizeDebounceTime > 1000
				) {
					this.config.resizeDebounceTime = 200; // Reset to default
				}

				this.utils.log("Configuration validated", this.config);
			} catch (error) {
				this.utils.error("Error validating configuration", error);
				// Reset to defaults
				this.config = {
					resizeDebounceTime: 200,
					breakpoints: {
						mobile: 480,
						tablet: 768,
						desktop: 1025,
						largeDesktop: 1440,
					},
					enableLogging: false,
					enableFeatureDetection: true,
					fallbackBrowserClass: "unknown-browser",
				};
			}
		},

		/**
		 * Initialize the detector with optional custom configuration
		 */
		init: function (customConfig) {
			try {
				if (this.state.initialized) {
					this.utils.log("Already initialized; call destroy() first to reinitialize");
					return this.state.currentBrowser;
				}

				this.utils.log("Initializing Browser Detector");
				this.validateConfig(customConfig);

				// Add basic initial class
				this.utils.addClass("js-enabled");

				// Run detection processes
				this.detectBrowser();

				if (this.config.enableFeatureDetection) {
					this.detectPlatformFeatures();
				}

				this.handleResponsive();

				// Set up resize listener with debouncing
				const debouncedResize = this.utils.debounce(
					this.handleResponsive.bind(this),
					this.config.resizeDebounceTime
				);

				// Store reference for cleanup
				this._resizeHandler = debouncedResize;
				window.addEventListener("resize", debouncedResize);

				// Mark as initialized
				this.state.initialized = true;

				// Add version class
				this.utils.addClass("browser-detection-active");

				this.utils.log("Browser Detector initialized", {
					browser: this.state.currentBrowser,
					state: this.state,
				});

				return this.state.currentBrowser;
			} catch (error) {
				this.utils.error("Failed to initialize Browser Detector", error);
				this.utils.addClass("initialization-error");
				return "unknown";
			}
		},

		/**
		 * Clean up resources and event listeners
		 */
		destroy: function () {
			try {
				if (!this.state.initialized) {
					this.utils.log("Not initialized; nothing to destroy");
					return false;
				}

				this.utils.log("Destroying Browser Detector");

				// Remove resize listener
				if (this._resizeHandler) {
					window.removeEventListener("resize", this._resizeHandler);
					this._resizeHandler = null;
				}

				// Remove status classes
				this.utils.removeClass("browser-detection-active");
				this.utils.removeClass("js-enabled");

				// Reset state
				this.state.initialized = false;
				this.state.currentBrowser = null;

				this.utils.log("Browser Detector destroyed");
				return true;
			} catch (error) {
				this.utils.error("Error destroying Browser Detector", error);
				return false;
			}
		},

		/**
		 * Public API to manually refresh detection
		 */
		refresh: function () {
			if (!this.state.initialized) {
				this.utils.error("Cannot refresh: not initialized");
				return false;
			}

			this.utils.log("Manually refreshing detection");
			this.detectBrowser();

			if (this.config.enableFeatureDetection) {
				this.detectPlatformFeatures();
			}

			this.handleResponsive();
			return true;
		},

		/**
		 * Public API to get current detection state
		 */
		getState: function () {
			return { ...this.state };
		},
	};

	// Initialize the detector and expose globally
	const currentBrowser = BrowserDetector.init();
	window.BrowserDetector = BrowserDetector;

	// Log initial detection results
	if (BrowserDetector.config.enableLogging) {
		console.log("[BrowserDetector] Detected:", {
			browser: currentBrowser,
			isMobile: BrowserDetector.state.isMobile,
			isTouch: BrowserDetector.state.isTouch,
		});
	}
});

🧩 Key Components

The code consists of several modules:

  • Browser detection methods

  • Platform detection functions

  • Feature detection capabilities

  • Responsive handling for different screen sizes

  • Utility functions for managing CSS classes

⚙️ Configuration Options

  • You can customize the script with options like:

  • Resize debounce time (how quickly it responds to window resizing)

  • Custom breakpoints for different screen sizes

  • Enabling/disabling certain detection features

  • Turning on logging for debugging

📱 Device Detection

The code specifically checks for:

  • Mobile devices (phones)

  • Tablets

  • Touch capabilities

  • Operating systems

  • Legacy browser support

🔄 Responsive Features

When a user resizes their browser:

  • The script detects the new size

  • Updates classes accordingly

  • Applies appropriate viewport classes

  • Does this efficiently with debouncing

📋 How To Implement

Add this script to your website, and it will automatically:

  • Run when the page loads

  • Detect all browser/device information

  • Add the appropriate classes

  • Set up listeners for window resizing

  • Make the detector available as window.BrowserDetector

Now your website can intelligently adapt to different browsing environments!


Simplified Version

If you don’t need all the advanced features and just want basic browser detection with responsive classes, here’s a lighter alternative version:

/**
 * Browser Detection Utility
 *
 * This script identifies the user's browser and applies appropriate classes to the document body.
 * It also provides responsive class handling for different screen sizes.
 *
 * Features:
 * - Detects major browsers (Safari, Firefox, Chrome, Edge, Opera)
 * - Applies browser-specific classes to body element
 * - Adds responsive classes for different screen sizes
 * - Uses a configurable, efficient event handling system
 */
document.addEventListener("DOMContentLoaded", function () {
	const BrowserDetector = {
		config: {
			resizeDebounceTime: 200,
			breakpoints: {
				mobile: 480,
				tablet: 768,
				desktop: 1025,
				largeDesktop: 1440,
			},
		},

		browsers: {
			safari: function () {
				const vendor = navigator.vendor || "";
				return (
					vendor.includes("Apple") &&
					!navigator.userAgent.includes("Chrome") &&
					!navigator.userAgent.includes("Edg") &&
					("ontouchend" in document || window.safari !== undefined)
				);
			},
			firefox: function () {
				return navigator.userAgent.includes("Firefox");
			},
			chrome: function () {
				const vendor = navigator.vendor || "";
				return (
					navigator.userAgent.includes("Chrome") &&
					vendor.includes("Google") &&
					!navigator.userAgent.includes("Edg")
				);
			},
			edge: function () {
				return navigator.userAgent.includes("Edg");
			},
			opera: function () {
				return navigator.userAgent.includes("OPR") || navigator.userAgent.includes("Opera");
			},
		},

		utils: {
			debounce: function (func, wait) {
				let timeout;
				return function (...args) {
					clearTimeout(timeout);
					timeout = setTimeout(() => func.apply(this, args), wait);
				};
			},
			addClass: function (className) {
				document.body.classList.add(className);
			},
			removeClass: function (className) {
				document.body.classList.remove(className);
			},
			toggleClass: function (className, condition) {
				document.body.classList.toggle(className, condition);
			},
		},

		detectBrowser: function () {
			document.body.className = document.body.className
				.split(" ")
				.filter((cls) => !cls.endsWith("-browser"))
				.join(" ");

			for (const [browser, detectFn] of Object.entries(this.browsers)) {
				if (detectFn()) {
					this.utils.addClass(`${browser}-browser`);
					this.currentBrowser = browser;
					return browser;
				}
			}

			this.utils.addClass("unknown-browser");
			this.currentBrowser = "unknown";
			return "unknown";
		},

		handleResponsive: function () {
			const width = window.innerWidth;
			const { breakpoints } = this.config;

			for (const size of Object.keys(breakpoints)) {
				this.utils.removeClass(`${this.currentBrowser}-${size}`);
			}

			if (width < breakpoints.mobile) {
				this.utils.addClass(`${this.currentBrowser}-mobile`);
			} else if (width < breakpoints.tablet) {
				this.utils.addClass(`${this.currentBrowser}-tablet`);
			} else if (width < breakpoints.desktop) {
				this.utils.addClass(`${this.currentBrowser}-tablet-large`);
			} else if (width < breakpoints.largeDesktop) {
				this.utils.addClass(`${this.currentBrowser}-desktop`);
			} else {
				this.utils.addClass(`${this.currentBrowser}-desktop-large`);
			}
		},

		init: function () {
			this.detectBrowser();
			this.handleResponsive();
			window.addEventListener(
				"resize",
				this.utils.debounce(this.handleResponsive.bind(this), this.config.resizeDebounceTime)
			);
			return this.currentBrowser;
		},
	};

	const currentBrowser = BrowserDetector.init();
	window.BrowserDetector = BrowserDetector;
});

This simplified version retains the core functionality while removing some advanced features. It still detects the browser and applies responsive classes based on screen size.

What’s Different in the Simplified Version?

The simplified version:

  1. Is about 75% smaller (only ~120 lines vs. ~500 lines)

  2. Focuses on the core browser detection functionality

  3. Still handles responsive classes based on screen size

  4. Omits advanced features like:

    • Mobile and tablet detection

    • OS detection

    • Touch capability detection

    • Legacy browser detection

    • Private browsing detection

    • Extensive error handling

    • Feature detection for browser identification

    • Configuration validation

This lightweight version is ideal for blogs or websites that just need basic browser detection without all the bells and whistles.

Example Usage

The simplified version still provides the same core browser classes:

/**
 * Browser-specific styling
 *
 * This SCSS handles browser-specific adjustments for various layouts and components
 * based on the classes applied by the BrowserDetector script.
 */

body {
	&.safari-browser {
		&.safari-desktop {
			/*  Safari on desktop devices */
		}

		&.safari-tablet,
		&.safari-mobile {
			/*  Safari on mobile and tablet devices */
		}
	}

	/*  Firefox-specific adjustments */
	&.firefox-browser {
		&.firefox-mobile {
			/* Add mobile Firefox styles here */
		}
		&.firefox-desktop {
			/* Add desktop Firefox styles here */
		}
	}

	/* Chrome-specific adjustments */
	&.chrome-browser {
		/* Add any Chrome-specific styles here */
	}

	/* Edge-specific adjustments */
	&.edge-browser {
		/* Add any Edge-specific styles here */
	}

	/* Opera-specific adjustments */
	&.opera-browser {
		/*  Add any Opera-specific styles here */
	}
}

Thanks for reading !!! 😊
Share this post :

Modern CSS Resets