Passably Explained - Browserslist
Posted on Aug 31, 2021 by Alexander Morse.
As the name would suggest, Browserslist is a fun tool for generating lists of browsers. Clearly, a list of browsers is its own reward, which makes its existence perfectly justified. But it turns out that these lists can be genuinely useful in their own right, so let's spend some time exploring how to make them, and how they tend to be used.
If you've worked with any kind of Javascript tooling before, like Babel or Autoprefixer, then you might have come across Browserslist indirectly. Often, you'll come across instructions to add something like this to your package.json
file:
{
// ...
"browserslist": [
"last 2 versions",
"not dead",
"ie 10"
]
}
Without prior experience, this can feel like magical gibberish. It isn't immediately clear what this array of strings is actually doing. Often, some documentation will go on to explain that you're defining a collection of "target browsers" that ensures your code will work on every target platform. This isn't untrue, but it might give the impression that Browserslist is doing more than it really is.
The truth is that Browserslist creates lists of browsers. It does literally nothing else, and that's the whole point. For now, let's focus on this core behavior.
By the way — yes, that's an 's' in the middle of Browserslist. Make sure to spell it right going forward.
Setup
Browserslist isn't the kind of tool most developers will use directly, but I personally think a hands-on approach is best for getting a feel for this sort of thing. To that end, we'll go ahead and make a new, exploratory Node.js project. From the console (and in your directory of choice), we can set it up like so:
mkdir browserslist-tests
cd browserslist-tests
npm init -y
npm install --save-dev browserslist
Then, we'll create a new file to play with Browserslist in. Something like browsers.js
in the root directory would work.
Basic Usage
Let's move right on the the exciting part: generating our first list of browsers. With no other configuration required, we can generate it with the following code:
// browsers.js
const browserslist = require("browserslist");
const browsers = browserslist();
console.log(browsers); // ["and_chr 92", "and_ff 90", ...]
Run this code (with node browsers.js
), and you'll find the call to browserslist()
has yielded an array of browser names! At the time of this writing, it yields a list of 28 different browsers. But why these 28? As it happens, each of these browsers meets at least one of the following criteria:
- It is used by at least 0.5% (half of one percent) of the global population.
- It is one of the latest two versions of each browser (Chrome, Firefox, etc)
- It is the one of the latest Firefox Extended Support Release versions.
- If it meets one of the above criteria, it has also seen official support or updates within the last 24 months.
This set of rules is called the "defaults" query, and Browserslist accepts it as a valid query string. In the previous code example, replace the browserslist()
call with browserslist("defaults")
, and you can confirm for yourself that you get the exact same output.
If the "defaults" syntax seems weirdly specific and arbitrary, that's because it is. But it turns out that if you select browsers in this way, a large percentage of the global population is guaranteed to be using at least one of them. How large of a percentage are we talking? I could just tell you, or we could use our new list of browsers to determine the coverage ourselves:
// browsers.js
const browserslist = require("browserslist");
const browsers = browserslist("defaults");
const coverage = browserslist.coverage(browsers);
console.log(coverage);
As of this writing, I get 91.95253010000002
, or about 92%. So if I'm ever feeling lazy about specifying browsers, I can just pass in "defaults" as my Browserslist query to get a reasonable set of supported browsers.
"Can I Use" Data
Saying that a list of browsers has 92% coverage is fine, but we'd be justified in asking how exactly Browserslist knows that. For that matter, how does it know for sure which version of each browser is the latest, or if a browser is recently-supported?
If you've done a lot of front-end web development, there's a good chance you're familiar with the Can I Use... website as a means for checking whether or not a particular browser feature is widely available to users. For example, check out this page breaking down browser support for CSS Grid. You'll notice the list of browsers on that page, as well as the percentage of browsers that support it.
The information on CanIUse is all populated by a data set made available on the caniuse Github repository. Browserslist uses this same data to resolve its queries, although it uses a more compact version of the data called caniuse-lite to do so.
That clears up where Browserslist is getting its data from, but now we might be just as curious as to what sources CanIUse is pulling from. For feature support, there isn't any higher authority to rely on — browser features are tracked manually and painstakingly over thousands of Github contributions. Browser usage statistics are courtesy of StatCounter GlobalStats, who track it as part of their web analytics service.
Browserslist Queries
At this point, we know how to get a list of "default" browsers with a decent coverage rate, and we've looked at the data Browserslist uses to justify that list. But maybe we're a more discerning sort of browser-list connoisseur, and we aren't satisfied with a simple default set of browsers. What if we wanted our list to contain Internet Explorer 10? Or if we want to target other types of platforms, like specific versions of Node.js?
That's where the Browserslist query language comes in. This won't be a complete tutorial of every query type, since I think the documentation is easy enough to follow. Even so, let's look at a few of the basics.
One of the easiest ways to query is by usage statistics. Remember how "defaults" included every browser with at least 0.5% market share? We can express that with the >= 0.5%
query, like so:
// browsers.js
const browserslist = require("browserslist");
const browsers = browserslist(">= 0.5%");
console.log(browsers);
Naturally, we could alter the query to specify a higher or lower percentage as the threshold, or use any of the other inequality operators, like greater-than (>
) or less-than-or-equal-to (<=
).
But that only gave us a list of 18 or so (as of this writing). The defaults included more browsers than that. For instance, it also made sure to include the last two versions of every major browser. That can be expressed with the last 2 versions
query:
// browsers.js
const browserslist = require("browserslist");
const browsers = browserslist("last 2 versions");
console.log(browsers);
That gives us a longer list — 30 or so. Finally, the defaults included the latest Firefox Extended Support Release. We can select these (and other browsers) by name with queries like Firefox ESR
:
// browsers.js
const browserslist = require("browserslist");
const browsers = browserslist("Firefox ESR");
console.log(browsers);
As of this writing, this query yields Firefox 91 and Firefox 78 as the latest ESR versions.
There was one other condition required by the "defaults" query: any browser returned by one of the previous queries must also have received recent support or updates within the last 24 months. At this point in the article, we don't know enough to make that specification work. However, we do have a query we can use to list unsupported browsers: dead
.
// browsers.js
const browserslist = require("browserslist");
const browsers = browserslist("dead");
console.log(browsers);
Query Composition
Now we've explored all the ways the "defaults" query goes about looking up browsers. There are four queries that it needs to combine somehow: >= 0.5%
, last 2 versions
, Firefox ESR
, and finally we don't want to include anything that shows up in the dead
query. We can do all of this with query composition.
The or
Operator
To start, let's handle the first three queries: >= 0.5%
, last 2 versions
, and Firefox ESR
. All we really want to do is merge the results of each of these, removing any duplicates in the list. We can do this with the or
syntax, which combines the queries on the left- and right-hand sides of the operator:
// browsers.js
const browserslist = require("browserslist");
const browsers = browserslist(">= 0.5% or last 2 versions or Firefox ESR");
console.log(browsers);
Run this, and you'll notice we get the union of all three queries. Most Browserslist compositions use or
so commonly that there are two different shorthands for it. The first shorthand is to use a comma (,
) in place of or
, letting us express the composition in a more natural, list-like format.
// browsers.js
const browserslist = require("browserslist");
const browsers = browserslist(">= 0.5%, last 2 versions, Firefox ESR");
console.log(browsers);
The second shorthand eschews operators altogether, and lets us specify an array of individual queries. Each query in the array will be strung together with an implicit or
:
// browsers.js
const browserslist = require("browserslist");
const browsers = browserslist(["> 0.5%", "last 2 versions", "Firefox ESR"]);
console.log(browsers);
You can confirm for yourself that each of these compositions yields the exact same list of browsers.
The not
Operator
We've now built a query that does almost the same thing as "defaults", but we're still missing the final specification: if a browser is dead (has not received support within the last 24 months), it should not appear in the returned list of browsers. Currently this isn't the case, since queries like last 2 versions
include dead browsers like Internet Explorer 10.
We can fix this with the not
operator. Be careful with this one, especially if you're familiar with operations on sets. not
is a subtraction operator, instead of negating the query like you might assume. That is, it only works if we give it something to subtract from.
In general, <query1>, not <query2>
evaluates <query1>
first, then evaluates <query2>
, and then resolves to <query1> - <query2>
, with anything in the latter list being excluded from the former. For example, try the composition: dead, not dead
. Since this resolves to dead - dead
, we should get an empty list. But since not
only operates on the left-hand side, the composition dead, not dead, dead
resolves to (dead - dead) + dead
, which is simply the list of dead browsers.
Long story short: use not
to specify which browsers you don't want in the final list, but only after you've specified which browsers you do want.
With this operator, we now know enough to write the entire "defaults" query:
// browsers.js
const browserslist = require("browserslist");
const browsers = browserslist(">= 0.5%, last 2 versions, Firefox ESR, not dead");
console.log(browsers);
...which will give us the exact same list as "defaults".
The and
Operator
There is one last operator that isn't used in "defaults", but is still worth mentioning. Just like or
is used to provide the union of two lists, and
is used to provide the intersection. That is, <query1> and <query2>
resolves to the list of browsers that show up in both <query1>
and <query2>
.
For example, take the Firefox Extended Support Release versions. Which of these, if any, is also one of the last two versions released? We can use and
to find out:
// browsers.js
const browserslist = require("browserslist");
const browsers = browserslist("Firefox ESR and last 2 versions");
console.log(browsers);
As of this writing, I get just one version back: Firefox 91.
More Queries
There are a number of other query types available to Browserslist. For example, we can select browsers by release year, or target different platforms like Node versions and headless browsers. Again, I suggest looking at the Browserslist documentation if you're interested in learning all the tricks.
But now that we've got a feel for the basics, let's move on to configuring Browserslist.
Configuring Browserslist
Remember the example I gave at the start of the article? Sometimes, a tool will direct you to place a Browserslist query in your package.json
file, like this:
{
// ...
"browserslist": [
"last 2 versions",
"not dead",
"ie 10"
]
}
At this point, we should be able to tell what this query is doing: it specifies the last two versions of each browser, except for the ones that are dead, and we also want to include Internet Explorer 10 for...some reason. You do you.
Now let's try something. Go ahead and add the above query (or one like it) to your project's package.json
file. Then run Browserslist without giving it a query:
// browsers.js
const browserslist = require("browserslist");
const browsers = browserslist();
console.log(browsers);
The last time we ran this code, we got the result of the defaults
query back. But now, it should look a little different — Browserslist should have automatically run the query given in package.json
.
This is actually a best practice, because a browserslist
field in package.json
makes a strong, project-wide statement: "These are the browsers I care about supporting". Up until now, we've just been playing with Browserslist, but in practice we'll rarely use it manually. Instead, the function will be called for us by other libraries, and it would be best if each of them was working from the same list of browsers.
Once again, this isn't the whole story for configuration. You can use a separate config file, for example, or supply different queries depending what kind of environment you're running code in. As usual, the documentation has all of this covered.
Uses for Browserslist
Now that we've got a handle on creating lists of browsers...what exactly did we want a list of browsers for, anyway? Personally I like to print them out and hang them on my refrigerator, but typically these lists are used for browser support. When we provide a browserslist
query to package.json
, we're making a project-wide statement about which browsers we want to support. And as it turns out, there are a number of tools that, if installed, are more than happy to listen to that statement.
A few examples:
- Autoprefixer transforms your CSS, extending browser support by adding vendor prefixes to certain style properties. But we might not need to prefix everything, so Autoprefixer will check your list of supported browsers and determine if a given prefix is necessary.
- Babel transforms Javascript via a series of plugins and presets that can make modern code runnable on older browsers. Unfortunately, we need to go to the trouble of picking and installing each of these plugins manually...unless we use babel-preset-env, which checks our supported browsers and figures it out for us. We can even set it to polyfill code for us.
- Certain linting tools, like eslint-plugin-compat can alert you when you try to make use of a feature that doesn't work in one of your supported browsers, which might be a hint that you need to install a polyfill.
It turns out that a simple array of browser versions can be used for quite a lot.
Custom Usage Data
As we covered earlier, Browserslist queries like defaults
are informed by the results of GlobalStats analytics. And this is good for determining the market share of browsers on average. But what if your users aren't average? For example, the users of a news site about modern PC gaming might be more likely to use the latest desktop browsers, and less likely to use Opera Mini.
Browserslist allows us to specify and query from our own local data. Unfortunately, we need to gather this data before we can use it, which means we'd want to use something like Google Analytics to track browser usage for our site. Depending on how we gather the data, different tools like browserslist-ga-export exist to convert it into a format Browserslist understands.
The format that Browserslist understands, then, is a file called browserslist-stats.json
, which should be located right in the root directory. Let's make our own. Rather than use anything realistic, we'll suppose our users only use a handful of browsers: Firefox, Chrome, and Edge, and even then only the newest versions of each. We can dream.
{
"firefox": {
"90": 5.3,
"91": 40.0
},
"chrome": {
"91": 10.7,
"92": 33.8
},
"edge": {
"91": 0.2,
"92": 10.0
}
}
Now, we can query against this data specifically (usually by percentage) using the <percentage> in my stats
query:
// browsers.js
const browserslist = require("browserslist");
const browsers = browserslist(">= 5% in my stats");
console.log(browsers);
Conclusion
For a tool that only creates lists of browsers, there are quite a few different aspects to consider if we want to use Browserslist thoughtfully. And we've only covered a handful of query types and applications, so I'd recommend reading the Browserslist documentation for the full story.
But hopefully we've seen just how useful it can be, once we know what it's doing. And after reading this, you'll no longer be confused the next time some tool or framework tells you to define a Browserslist query. At the end of the day, it's not asking for much at all — just a list of browsers.