Why browser detection matters
When working with contenteditable, browser differences are not just cosmetic—they directly affect how your code works:
- Event sequences differ (e.g.,
beforeinputsupport) - IME composition behavior varies
- Selection and Range API implementation differences
- Undo/redo stack behavior differs
Detecting the browser allows you to apply workarounds for known issues and provide consistent behavior across platforms.
Understanding User-Agent strings
The navigator.userAgent string contains information about the browser and platform. However, on iOS, things get complicated:
Safari (iOS)
Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1
Key identifiers: Version/16.0, Safari/604.1, NO CriOS
Chrome (iOS)
Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/115.0.5790.130 Mobile/15E148 Safari/604.1
Key identifiers: CriOS/115.0.5790.130, Safari/604.1 (still present)
iOS uses the same rendering engine: Chrome, Firefox, and other browsers on iOS are required by Apple to use WebKit (Safari's engine). This means rendering behavior is identical, but JavaScript APIs and bugs may still differ.
Browser detection patterns
There are several approaches to browser detection. Choose based on your use case.
Flag-based detection
Parse the User-Agent string and set boolean flags. Simple but fragile if user agents change.
function getBrowserFlags(ua: string) {
return {
isSafari: /Safari\//.test(ua) && !/CriOS/.test(ua),
isChrome: /CriOS/.test(ua),
isFirefox: /FxiOS/.test(ua),
engine: 'webkit' as const // iOS uses WebKit
};
}Behavior-based detection (recommended)
Test for the presence of APIs you need. More reliable because it detects actual capabilities.
function getBrowserFeatures() {
return {
supportsBeforeInput: 'onbeforeinput' in document,
supportsComposition: 'oncompositionstart' in document,
supportsSelectionAPI: !!window.getSelection
};
}Use this when you care about what the browser can do, not which browser it is.
Hybrid approach
Combine both: detect the browser for known workarounds, but use feature detection for core functionality.
function getBrowserInfo() {
const ua = navigator.userAgent;
return {
os: /iPhone|iPad|iPod/.test(ua) ? 'ios' : 'android',
browser: /CriOS/.test(ua) ? 'chrome-ios' :
/Safari\//.test(ua) ? 'safari' : 'unknown',
engine: 'webkit', // iOS always uses WebKit
needsSpecialHandling: /CriOS/.test(ua)
};
}Best practices
Avoid over-detection: Only detect browsers when you have a specific, known issue to work around. Don't add "just in case" browser checks.
Prefer feature detection: If you need to know if beforeinput is supported, check 'onbeforeinput' in document, not the browser name.
Handle iOS carefully: Remember that Chrome and Safari on iOS share the same engine. Test behaviors, not just names.
Document your workarounds: When you add a browser-specific workaround, add comments explaining why it's needed. This helps when browsers fix issues or behavior changes.
Firefox IME duplication example
Firefox can fire duplicate compositionupdate events for Korean IME input. Filter them by tracking the last event:
let lastCompositionType = '';
let lastCompositionData = '';
let isComposing = false;
function handleCompositionUpdate(e: CompositionEvent) {
// Filter out duplicate events
if (e.data === lastCompositionData && e.type === lastCompositionType) {
return;
}
lastCompositionData = e.data;
lastCompositionType = e.type;
// Process the event
}User-Agent reference
Common User-Agent strings from various browsers and operating systems. These are examples from recent versions; actual strings will vary by version number and build.
macOS
| Browser | Engine | Key Identifiers |
|---|---|---|
| Safari 17 Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15 | WebKit | No Chrome/Edg/Firefox identifiers |
| Chrome 120 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 | Blink | Contains "Chrome/" and "Safari/" (Safari string is for compatibility) |
| Firefox 121 Mozilla/5.0 (Macintosh; Intel Mac OS X 14.0; rv:121.0) Gecko/20100101 Firefox/121.0 | Gecko | Contains "Firefox/" and "Gecko/" |
Windows
| Browser | Engine | Key Identifiers |
|---|---|---|
| Chrome 120 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 | Blink | "Windows NT 10.0" = Windows 10/11 |
| Edge 120 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0 | Blink | Contains "Edg/" (Chrome-based Edge) |
| Firefox 121 Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0 | Gecko | "Windows NT 10.0" for both Windows 10/11 |
Linux
| Browser | Engine | Key Identifiers |
|---|---|---|
| Chrome 120 Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 | Blink | "X11" for desktop Linux |
| Firefox 121 Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0 | Gecko | Uses "X11" instead of specific Linux distribution |
Mobile (iOS & Android)
| Platform | Browser | Engine | Key Identifiers |
|---|---|---|---|
| iOS | Safari 17 Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1 | WebKit | iOS browsers must use WebKit |
| iOS | Chrome 120 Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/120.0.6099.119 Mobile/15E148 Safari/604.1 | WebKit | "CriOS/" identifies Chrome on iOS |
| iOS | Firefox 121 Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/121.0 Mobile/15E148 Safari/604.1 | WebKit | "FxiOS/" identifies Firefox on iOS |
| Android | Chrome 120 Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36 | Blink | "Mobile Safari/" is legacy, Android Chrome uses Blink |
| Android | Samsung Internet 23 Mozilla/5.0 (Linux; Android 14; SM-S911B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/23.0 Chrome/120.0.0.0 Mobile Safari/537.36 | Blink | "SamsungBrowser/" + Chrome version |
| Android | Firefox 121 Mozilla/5.0 (Android 14; Mobile; rv:121.0) Gecko/121.0 Firefox/121.0 | Gecko | Firefox on Android uses Gecko |