ESC

Type to search the knowledge base.

Feature Detection vs UA Sniffing

Prefer feature detection over user-agent parsing — @supports, 'in' checks, and when UA hints still appear in the wild.

beginner3 min read
  • javascript
  • feature-detection
  • user-agent
  • compatibility

User-agent sniffing guesses the browser from a string and branches. Feature detection asks: does this environment support the capability I need? Sniffing breaks when engines share code, spoof strings, or ship the feature you blocked. Detection ages better.

Feature detection

// API exists?
if ('IntersectionObserver' in window) {
  const io = new IntersectionObserver(callback);
} else {
  // scroll fallback or polyfill
}

// method behavior
if (typeof structuredClone === 'function') {
  data = structuredClone(obj);
} else {
  data = JSON.parse(JSON.stringify(obj));
}

// CSS
if (CSS.supports('display', 'grid')) {
  el.classList.add('supports-grid');
}
@supports (display: grid) {
  .layout {
    display: grid;
  }
}

Test the thing you will call, not a brand name.

UA sniffing (why it’s fragile)

// brittle
if (navigator.userAgent.includes('Chrome')) {
  enableFancy();
}

Problems:

  1. Chromium shows up in Edge, Opera, Electron, WebViews.
  2. Safari/iOS share engines across apps.
  3. Strings change; crawlers lie.
  4. You block a browser that already shipped the feature — or allow one that didn’t.
// slightly less wrong but still not capability testing
const isIOS =
  /iPad|iPhone|iPod/.test(navigator.userAgent) ||
  (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
// still a sniff — use only for known platform bugs with no feature test

When sniffing still appears

  • Fixing a specific engine bug with no reliable capability probe
  • Analytics / bot filters (separate threat model)
  • Enterprise quirks documentation

Even then, isolate the hack, comment the bug ID, and prefer @supports / API checks when they exist.

// example: existence isn't enough — behavior test
function supportsPassive() {
  let passive = false;
  try {
    const opts = Object.defineProperty({}, 'passive', {
      get() {
        passive = true;
        return false;
      },
    });
    window.addEventListener('test', null, opts);
    window.removeEventListener('test', null, opts);
  } catch (_) {}
  return passive;
}

Client Hints reduce free-form UA strings but are still identity, not capability:

// Chromium
const brands = navigator.userAgentData?.brands;

Use for metrics maybe; don’t gate features solely on brand.

Progressive enhancement pattern

async function share(data) {
  if (navigator.share) {
    return navigator.share(data);
  }
  await navigator.clipboard.writeText(data.url);
  toast('Link copied');
}

Offer a baseline; upgrade when APIs exist.

Interview answer

“Feature detection checks whether the API or CSS feature exists or behaves correctly; UA sniffing parses navigator.userAgent and guesses. Detection is preferred because strings lie and engines fork. I use in/window checks, CSS.supports, and behavioral probes; I only sniff for documented engine bugs with no feature test.”

CSS and HTML detection

// input types
const i = document.createElement('input');
i.setAttribute('type', 'date');
const supportsDate = i.type === 'date';

// media queries as feature tests
const finePointer = matchMedia('(pointer: fine)').matches;
const prefersReducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;

Capability detection extends beyond JS APIs — input types, media queries, and @supports cover UI decisions. Prefer prefers-reduced-motion over sniffing Safari version to disable animations. Keep a tiny matrix of supported baselines (browserslist) so you polyfill deliberately rather than branching on every UA.

Polyfill loading strategy

async function ensureIntlListFormat() {
  if (typeof Intl.ListFormat === 'function') return;
  await import('@formatjs/intl-listformat/polyfill');
}

Detection + conditional dynamic import loads polyfills only when needed. That pairs with browserslist baselines: ship modern code, polyfill the long tail. Never gate a CSS layout solely on navigator.userAgent when @supports exists.

Further reading

Related guides