Back to blog

Why Browsers Claim to Be Mozilla

Every browser's user agent string is a pile of borrowed names left over from a 1990s fight over frames. Here's how to read one, and why feature detection beats it.

A vintage computer monitor left out in the open at Cisco Ghost Town in Utah.
Photo by Nick Russill on Unsplash

Open DevTools in Chrome and type navigator.userAgent. You'll get something close to this.

Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36

That line mentions Mozilla, WebKit, KHTML, Gecko, and Safari. The browser is none of them. By the end of this post you'll be able to explain every word in that string, and you'll know why your code should almost never read it.

A user agent string is the label a browser sends in the User-Agent HTTP header with each request. JavaScript can read the same value. It was meant to say which browser is calling. It turned into a thirty-year record of browsers pretending to be each other.

It started with a fight over frames

In the early 1990s, the popular browser was NCSA Mosaic. It identified itself plainly.

NCSA_Mosaic/2.0 (Windows 3.1)

Then Netscape showed up. Its internal code name was Mozilla, short for "Mosaic Killer," and that name went into the string even after the public name became Netscape.

Mozilla/1.0 (Win3.1)

Netscape supported frames, a way of splitting a page into separately loaded panes. Mosaic did not. So webmasters started checking the header and sending frames only to browsers that said Mozilla. That check is called user agent sniffing, and the web has never fully recovered.

Internet Explorer arrived with frames of its own, but it didn't say Mozilla, so sites served it the frameless version. Microsoft's fix was to say Mozilla anyway, and to add a word that made it sound like less of a lie.

Mozilla/1.22 (compatible; MSIE 2.0; Windows 95)

Microsoft's own docs later summed it up in one wonderfully brief line. "For historical reasons, Internet Explorer identifies itself as a Mozilla browser."

Every new engine repeated the trick

An engine is the part of a browser that turns HTML and CSS into pixels. Each new engine had the same problem as IE. Sites sniffed for the current favorite and shut everyone else out.

When the Mozilla project built the Gecko engine, sites started sniffing for Gecko. Konqueror, which ran an engine called KHTML, responded by adding the phrase (KHTML, like Gecko). Apple then built WebKit on top of KHTML, and Safari kept all of it.

Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.5

Chrome launched on WebKit and wanted the pages built for Safari, so it added Safari to the end and put its own name in the middle. Nothing ever got removed, because removing a token risked breaking some site that still checked for it.

What each token means now

Here's the Chrome string again, token by token.

  • Mozilla/5.0 means nothing. Every major browser sends it.
  • (Windows NT 10.0; Win64; x64) is the platform, and it's frozen to a short list of values.
  • AppleWebKit/537.36 is a fossil. The number no longer changes.
  • (KHTML, like Gecko) is a compatibility claim about two engines Chrome doesn't use.
  • Chrome/130.0.0.0 is the only part doing real work, and only the major version is real.
  • Safari/537.36 is another fossil.

Those zeros are deliberate. Chrome's User-Agent Reduction project zeroed the minor version in Chrome 101, then reduced the desktop string in Chrome 107 and the Android string in Chrome 110. Detailed strings help sites fingerprint people, and the HTTP spec itself says a user agent should not send "needlessly fine-grained detail."

Firefox has its own fossil. On desktop, the Gecko token is the fixed string Gecko/20100101.

My favorite detail lives in the HTML Standard. It requires navigator.appName to return "Netscape" and navigator.product to return "Gecko". Try both in Chrome. A browser long since discontinued is still written into the spec, because old scripts might check.

Check for the feature, not the name

Here's the concrete takeaway. Sniffing asks the wrong question. You don't care which browser this is. You care if a specific thing works.

// Before, guessing from the name
if (navigator.userAgent.includes("Chrome")) {
  panel.showPopover();
}

This matches Edge and other Chromium browsers because their strings contain Chrome too. It skips Firefox and Safari, even though the Popover API has been Baseline since January 2025. Baseline means it works in the current versions of all the major browsers. It also throws a TypeError in an old Chrome that predates the API.

// After, asking about the feature
if ("showPopover" in HTMLElement.prototype) {
  panel.showPopover();
} else {
  panel.hidden = false;
}

Now the code is correct in browsers that don't exist yet. CSS has the same tool built in with @supports.

.progress {
  transform: scaleX(1);
}

@keyframes grow {
  from {
    transform: scaleX(0);
  }
}

@supports (animation-timeline: scroll()) {
  .progress {
    animation: grow linear;
    animation-timeline: scroll();
  }
}

MDN is blunt about the string approach, calling it "very hard to do reliably" and "often a cause of bugs." Reading the header still makes sense for analytics, or for working around one known bug in one known version. Keep those cases rare and tie them to a specific version.

The replacement expects the lie

The newer User-Agent Client Hints API gives structured data through navigator.userAgentData, in secure contexts. MDN still lists it as experimental with limited availability, so treat it as optional. Its most interesting feature is a fake entry.

navigator.userAgentData?.brands;
// In Chromium, something like
// [{ brand: "Chromium", version: "153" }, { brand: "Not_A Brand", version: "8" }, ...]

That Not_A Brand entry is on purpose. The spec requires user agents to include an arbitrary value and to change the order of the list over time. The idea comes from GREASE, a technique network protocols use to keep servers from depending on an exact list of values. The spec's reasoning is great reading for anyone who just learned this history. "History has shown us that there are real incentives for user agents to lie about their branding."

So the designers of the new API built the lying in on purpose, hoping nobody can write a brittle check against it.

The short version

The Mozilla at the front of every user agent string is a leftover from servers that wanted to send frames only to Netscape. Each browser after that copied the names it needed to get past those checks, and the string kept growing. When someone asks why Chrome claims to be Mozilla, Safari, and Gecko at once, you can now tell them exactly why. Then go write if ("thing" in object) and leave the string alone.

Sources