Browser detection & User-Agent

How to detect browsers and handle userAgent variations, especially on iOS.

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., beforeinput support)
  • 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