StackPractices
intermediate By Mathias Paulenko

Browser Support Matrix Template

A template for tracking browser support targets, feature compatibility, polyfill requirements, and fallback strategies across the browser matrix.

Topics: testing

Overview

A browser support matrix defines which browsers and versions your application supports, what features are available in each, and what polyfills or fallbacks are needed. Without one, teams make ad-hoc decisions that lead to inconsistent experiences and painful debugging — someone ships :has() because it works on their machine, and Safari 15 users get a broken page.

This template turns those scattered decisions into a single document: support tiers, a feature compatibility matrix, the polyfill strategy, the testing plan, and the upgrade-banner policy. Fill it once, review it quarterly.

When to Use

Use this template when:

  • Starting a new web project and the support baseline isn’t written down
  • Defining support requirements for a client project
  • Evaluating whether to drop support for an older browser
  • Planning polyfill and transpilation strategy
  • Communicating support decisions to stakeholders

It pairs naturally with the frontend performance budget template — the polyfills this matrix approves count against that budget — and the accessibility audit checklist for the assistive-technology side of compatibility.

Prerequisites

  • Analytics access: real browser/version usage data for your audience
  • An agreement on the usage threshold that justifies support (common: > 0.5%)
  • A list of the features your app actually uses (or plans to)
  • A testing tool decision: CI runner (Playwright) plus a real-device service (BrowserStack, LambdaTest)

Solution

# Browser Support Matrix — `<Project Name>`

## Matrix Overview

| Field | Value |
|-------|-------|
| Project | Example Web App |
| Last Updated | 2026-09-21 |
| Owner | Frontend Team |
| Support Strategy | Progressive enhancement |
| Baseline | Last 2 versions + > 0.5% global usage |
| IE11 Support | No |
| Transpilation Target | ES2020 |
| CSS Transpilation | PostCSS + Autoprefixer |

> Usage percentages below are example values — replace them with your own analytics.

## 1. Supported Browsers

### Tier 1 — Full Support

| Browser | Minimum Version | Global Usage | Testing | Notes |
|---------|----------------|-------------|---------|-------|
| Chrome | 110+ | 65.2% | Automated + manual | Evergreen, auto-updates |
| Edge | 110+ | 5.2% | Automated | Chromium-based, same as Chrome |
| Firefox | 115+ | 3.1% | Automated + manual | ESR + stable |
| Safari | 16+ | 4.3% | Manual + BrowserStack | macOS + iOS |

### Tier 2 — Best Effort Support

| Browser | Minimum Version | Global Usage | Testing | Notes |
|---------|----------------|-------------|---------|-------|
| Samsung Internet | 20+ | 2.6% | Manual | Android, Chromium-based |
| Opera | 95+ | 2.1% | Not tested | Chromium-based, assumed same as Chrome |
| Chrome (Android) | 110+ | 65.2% (mobile) | Manual | Mobile Chrome |

### Tier 3 — Not Supported

| Browser | Version | Global Usage | Notes |
|---------|---------|-------------|-------|
| Internet Explorer | All | 0.3% | EOL June 2022, not supported |
| Safari < 16 | < 16 | 0.8% | Show upgrade banner |
| Chrome < 110 | < 110 | 0.4% | Show upgrade banner |
| Firefox < 115 | < 115 | 0.2% | Show upgrade banner |

## 2. Feature Compatibility Matrix

### JavaScript Features

| Feature | Chrome 110+ | Edge 110+ | Firefox 115+ | Safari 16+ | Samsung 20+ | Polyfill Needed | Fallback |
|---------|------------|-----------|-------------|-----------|------------|----------------|----------|
| ES2020 (optional chaining, nullish coalescing) | ✅ | ✅ | ✅ | ✅ | ✅ | No | — |
| ES2021 (logical assignment, Promise.any) | ✅ | ✅ | ✅ | ✅ | ✅ | No | — |
| ES2022 (top-level await, class fields) | ✅ | ✅ | ✅ | ✅ | ✅ | No | — |
| ES2023 (array findLast, hashbang) | ✅ | ✅ | ✅ | ✅ | ✅ | No | — |
| structuredClone | ✅ | ✅ | ✅ | ✅ | ✅ | No | — |
| Intl.Segmenter | ✅ | ✅ | ✅ | ✅ | ✅ | No | — |
| Import maps | ✅ | ✅ | ✅ | ✅ | ✅ | No | — |
| CSS Modules (import attributes) | ⚠️ 123+ | ⚠️ 123+ | ❌ | ❌ | ⚠️ 123+ | Yes | Bundler handles |
| Web Components (custom elements, shadow DOM) | ✅ | ✅ | ✅ | ✅ | ✅ | No | — |
| ResizeObserver | ✅ | ✅ | ✅ | ✅ | ✅ | No | — |
| IntersectionObserver | ✅ | ✅ | ✅ | ✅ | ✅ | No | — |
| Web Workers | ✅ | ✅ | ✅ | ✅ | ✅ | No | — |
| Service Workers | ✅ | ✅ | ✅ | ✅ | ✅ | No | — |
| WebAssembly | ✅ | ✅ | ✅ | ✅ | ✅ | No | — |
| WebGPU | ⚠️ 113+ | ⚠️ 113+ | ❌ | ❌ | ⚠️ 113+ | Yes | WebGL fallback |

### CSS Features

| Feature | Chrome 110+ | Edge 110+ | Firefox 115+ | Safari 16+ | Samsung 20+ | Polyfill Needed | Fallback |
|---------|------------|-----------|-------------|-----------|------------|----------------|----------|
| CSS Grid | ✅ | ✅ | ✅ | ✅ | ✅ | No | — |
| Flexbox | ✅ | ✅ | ✅ | ✅ | ✅ | No | — |
| Custom Properties (CSS variables) | ✅ | ✅ | ✅ | ✅ | ✅ | No | — |
| Container Queries | ✅ | ✅ | ✅ | ✅ | ✅ | No | Media query fallback |
| :has() selector | ✅ 105+ | ✅ 105+ | ✅ 121+ | ✅ 15.4+ | ✅ | No | Class-based fallback |
| Cascade Layers (@layer) | ✅ | ✅ | ✅ | ✅ | ✅ | No | Specificity fallback |
| Color-mix() | ✅ 111+ | ✅ 111+ | ✅ 113+ | ✅ 16.2+ | ✅ | No | Pre-computed values |
| Nesting | ✅ 112+ | ✅ 112+ | ✅ 117+ | ✅ 16.5+ | ✅ | No | PostCSS nesting |
| Subgrid | ✅ 117+ | ✅ 117+ | ✅ 71+ | ✅ 16+ | ✅ | No | Regular grid fallback |
| View Transitions API | ✅ 111+ | ✅ 111+ | ❌ | ❌ | ✅ | Yes | No transition |
| Scroll-driven animations | ✅ 115+ | ✅ 115+ | ❌ | ❌ | ✅ | Yes | JS-based scroll |

### Web APIs

| Feature | Chrome 110+ | Edge 110+ | Firefox 115+ | Safari 16+ | Samsung 20+ | Polyfill Needed | Fallback |
|---------|------------|-----------|-------------|-----------|------------|----------------|----------|
| Fetch API | ✅ | ✅ | ✅ | ✅ | ✅ | No | — |
| Web Crypto API | ✅ | ✅ | ✅ | ✅ | ✅ | No | — |
| Web Storage (localStorage, sessionStorage) | ✅ | ✅ | ✅ | ✅ | ✅ | No | — |
| IndexedDB | ✅ | ✅ | ✅ | ✅ | ✅ | No | — |
| Cache API | ✅ | ✅ | ✅ | ✅ | ✅ | No | — |
| Push API | ✅ | ✅ | ✅ | ⚠️ 16.4+ | ✅ | No | Polling fallback |
| Web Notifications | ✅ | ✅ | ✅ | ⚠️ 16.4+ | ✅ | No | In-app notifications |
| Geolocation | ✅ | ✅ | ✅ | ✅ | ✅ | No | — |
| Web Share API | ✅ | ✅ | ❌ | ✅ | ✅ | Yes | Custom share dialog |
| File System Access API | ✅ | ✅ | ❌ | ❌ | ✅ | Yes | Download/upload fallback |
| Web Authentication (WebAuthn) | ✅ | ✅ | ✅ | ✅ | ✅ | No | — |
| Payment Request API | ✅ | ✅ | ❌ | ✅ | ✅ | Yes | Traditional checkout |

## 3. Polyfill Strategy

### Required Polyfills

| Polyfill | Target Browsers | Bundle Size (gzipped) | Load Strategy | Reason |
|----------|----------------|----------------------|---------------|--------|
| core-js (ES features) | All | 12 KB | Build-time | ES feature transpilation |
| regenerator-runtime | All | 3 KB | Build-time | Async/await transpilation |
| web-share-polyfill | Firefox | 2 KB | Dynamic import | Web Share API |
| webgpu-polyfill | Firefox, Safari | 8 KB | Dynamic import | WebGPU fallback to WebGL |

### Polyfill Loading Configuration

```javascript
// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', {
      targets: {
        chrome: '110',
        edge: '110',
        firefox: '115',
        safari: '16',
        samsung: '20',
      },
      useBuiltIns: 'usage',
      corejs: 3,
    }],
  ],
};
```

```javascript
// postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer')({
      overrideBrowserslist: [
        'Chrome >= 110',
        'Edge >= 110',
        'Firefox >= 115',
        'Safari >= 16',
        'Samsung >= 20',
      ],
    }),
    require('postcss-nesting'),
  ],
};
```

### Conditional Polyfill Loading

```javascript
// Dynamic polyfill loading for Web Share API
async function share(data) {
  if (navigator.share) {
    try {
      await navigator.share(data);
    } catch (err) {
      if (err.name !== 'AbortError') {
        console.error('Share failed:', err);
      }
    }
  } else {
    const { sharePolyfill } = await import('./polyfills/web-share.js');
    sharePolyfill(data);
  }
}

// Feature detection for WebGPU
async function getRenderingContext(canvas) {
  if (await navigator.gpu?.requestAdapter()) {
    const context = canvas.getContext('webgpu');
    return { type: 'webgpu', context };
  }
  const context = canvas.getContext('webgl2') || canvas.getContext('webgl');
  return { type: 'webgl', context };
}
```

## 4. Fallback Strategies

| Feature | Fallback Strategy | User Impact | Implementation Effort |
|---------|------------------|-------------|----------------------|
| View Transitions | No transition, instant page swap | No animation | Low — just don't use |
| Scroll-driven animations | JS IntersectionObserver-based | Slightly different timing | Medium |
| Web Share API | Custom share dialog with copy link | Different UI, same function | Medium |
| File System Access | Download + upload flow | Different workflow | Medium |
| WebGPU | WebGL renderer | Lower performance for 3D | High |
| Payment Request API | Traditional checkout form | More steps, same function | Low |
| Push API (Safari < 16.4) | Polling for updates | Battery impact, less real-time | Medium |

## 5. Testing Matrix

| Browser | Version | OS | Testing Method | Frequency | Coverage |
|---------|---------|-----|---------------|-----------|----------|
| Chrome | Latest | Windows | Playwright CI | Every PR | E2E + visual |
| Chrome | Latest | macOS | Playwright CI | Every PR | E2E + visual |
| Chrome | Latest | Android | BrowserStack | Weekly | Smoke test |
| Edge | Latest | Windows | Playwright CI | Every PR | E2E |
| Firefox | Latest | Windows | Playwright CI | Every PR | E2E |
| Firefox | Latest | macOS | Manual | Bi-weekly | Smoke test |
| Firefox | ESR | Windows | Manual | Monthly | Smoke test |
| Safari | Latest | macOS | BrowserStack | Weekly | E2E + visual |
| Safari | Latest | iOS | BrowserStack | Weekly | Smoke test |
| Safari | 16.0 | iOS | BrowserStack | Monthly | Smoke test |
| Samsung Internet | Latest | Android | BrowserStack | Monthly | Smoke test |

## 6. Build Configuration

### Browserslist Configuration

```text
[production]
Chrome >= 110
Edge >= 110
Firefox >= 115
Safari >= 16
Samsung >= 20

[development]
last 2 Chrome versions
last 2 Firefox versions
last 2 Safari versions
```

### Vite Configuration

```javascript
// vite.config.js
import { defineConfig } from 'vite';
import autoprefixer from 'autoprefixer';

export default defineConfig({
  build: {
    target: 'es2020',
    css: {
      postcss: {
        plugins: [autoprefixer()],
      },
    },
  },
  legacy: {
    targets: ['Chrome >= 110', 'Firefox >= 115', 'Safari >= 16'],
    additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
  },
});
```

## 7. Upgrade Banner

For browsers below the minimum supported version, show an upgrade banner. The banner uses a feature probe rather than user-agent parsing — a browser that can't run `structuredClone` is below every baseline in this matrix anyway:

```html
<div id="upgrade-banner" hidden>
  <p>Please update your browser to use this application.</p>
</div>

<script>
  // Feature probe: browsers that lack these are below our baseline.
  // (UA sniffing is fragile; the banner only needs a coarse signal.)
  if (!window.structuredClone || !window.fetch || !('loading' in HTMLImageElement.prototype)) {
    document.getElementById('upgrade-banner').hidden = false;
  }
</script>
```

Setting the Baseline

Everything in the matrix hangs off one decision: where the support line sits. Three inputs decide it:

  1. Your analytics, not global stats. Global usage puts Safari around 20% and Samsung Internet around 2%, but your audience is what matters — an internal enterprise tool might see 80% Chrome, while a consumer app in India sees KaiOS and Opera Mini. Pull the last 90 days of real browser/version traffic before writing a single tier.
  2. A usage threshold. Pick the number that justifies a tier — commonly >0.5% of sessions for Tier 1, >0.1% for Tier 2, below that a banner. The exact number matters less than writing it down: “we support what we measure” is a policy; “we support whatever people happen to use” isn’t.
  3. The evergreen floor. Most modern baselines use “last 2 versions” of each major browser — Browserslist understands this directly, so your build config and your document can share the same sentence.

Once the baseline is set, the tier assignment is mechanical: browsers at or above the threshold with full feature support go to Tier 1; browsers above the threshold but sharing an engine with a Tier 1 browser (Samsung Internet, Opera, Android Chrome vs. Chrome) go to Tier 2 with periodic smoke tests; everything else gets the banner.

How to Fill It In

Section 1 (Supported Browsers) is a data table, not an opinion table. Every row needs a minimum version, a measured usage figure from your analytics, and a testing method that actually exists — if nobody owns a Samsung device, write “BrowserStack” not “manual.”

Section 2 (Feature Compatibility) is the section that goes stale fastest, so scope it: only list features your app uses or plans to use in the next two quarters. A matrix that tracks all of CSS is unmaintainable; one that tracks your 15 actual dependencies is a decision tool. Each gap gets exactly one of two outcomes — a polyfill or a fallback — never both, and never blank.

Sections 3–4 (Polyfills and Fallbacks) are a budget negotiation. Every polyfill row carries its gzipped size because those bytes count against the performance budget — if the polyfill costs more than the feature is worth to the affected share of users, choose the fallback instead.

Section 5 (Testing Matrix) is what makes the support claim real. If Safari iOS is Tier 1 but nobody tests it, the matrix is fiction — either fund the BrowserStack time or demote the tier. The honest version of this section names the tool, the frequency, and the coverage for every row.

Section 7 (Upgrade Banner) is the polite exit for Tier 3. Keep it dismissible and informative — a banner that says “your browser is old” with no upgrade link just annoys people on locked-down corporate machines who can’t upgrade anyway.

Two habits keep the document honest once it’s written. Version it with the code — the matrix lives in the repo next to .browserslistrc, and a change to one should come with a change to the other in the same PR. And date every number — usage percentages and minimum versions age fast; a figure without a “as of” date is a claim nobody can verify.

How the Tiers Work in Practice

The template separates the decision (tier assignment) from the mechanism (polyfills and fallbacks) from the enforcement (testing matrix and banner). A browser lands in a tier based on your analytics, not on preference — a browser at 3% usage with your audience gets full support; the same browser at 0.05% gets a banner.

Tier 1 browsers get full testing: automated E2E, visual regression, and manual passes. Bugs there block release. Tier 2 browsers get best-effort testing — they’re usually Chromium-based and assumed to behave like Chrome, but they still get periodic smoke tests. Tier 3 browsers get the upgrade banner and nothing else.

The feature compatibility matrix is where tiers meet code. Every feature the app uses gets a row, and features with gaps get one of two treatments: polyfill (add bytes to support the feature) or fallback (degrade gracefully). Polyfills fit small, critical features; fallbacks fit large or non-critical ones. A WebGPU viewer degrades to WebGL; a share button degrades to a copy-link dialog — neither is worth 30 KB of polyfill for 2% of users.

The polyfill strategy then documents which polyfills exist, their size, and how they load. Build-time polyfills (Babel useBuiltIns: 'usage') ride in the main bundle. Dynamic polyfills load on demand through import() — users on modern browsers never pay for bytes they don’t need.

Polyfill or Fallback: The Decision

Every gap in the feature matrix resolves to one of these. The deciding factors, in order:

  1. How many users does the gap affect? A feature missing in a browser with 0.5% of your traffic gets the cheap option. A gap in a Tier 1 browser (rare, but Safari plays this role often) justifies real engineering.
  2. How big is the polyfill? core-js for a missing Array.prototype method costs a few hundred bytes after tree-shaking; a WebGPU shim costs megabytes and still won’t work well. Check the gzipped weight before committing — the polyfill column exists precisely to force that check.
  3. Can the feature degrade gracefully? View Transitions degrade to instant page swaps at zero cost — a perfect fallback. Web Share degrades to a copy-link dialog — still a fallback, but a user-visible one that needs design time. IndexedDB can’t degrade at all; if your app stores offline data, that’s a polyfill or a “not supported” call.

When both options are ugly, there’s a third door the template doesn’t list explicitly: don’t use the feature. Choosing a different implementation (CSS scroll-snap instead of scroll-driven animations) is often the cheapest compatibility strategy of all.

A Filled-In Example

A mid-size SaaS product pulling its last 90 days of analytics sees: Chrome 71%, Safari 18% (split between macOS and iOS), Edge 4%, Firefox 3%, Samsung Internet 1.5%, everything else under 0.2%. With a 0.5% threshold, that’s four Tier 1 browsers and one Tier 2. IE11 shows up at 0.1% — all from a single client pilot — so it lands in Tier 3 with a banner rather than a support commitment, which is the negotiation the matrix exists to make explicit.

The feature matrix flags two real gaps: Push API needs Safari 16.4 (their iOS users on 16.0–16.3 are 4% of traffic — worth the in-app-notification fallback) and File System Access is absent in Firefox and Safari (the export flow degrades to download/upload — same outcome, different path). WebGPU stays experimental behind a WebGL fallback because the 3D preview is a nice-to-have, not the product.

Cost projection: the polyfill list totals ~15 KB gzipped, all dynamic-loaded except core-js; the Safari push fallback is two days of work; BrowserStack time for Safari and mobile is already in the budget. Total: a document the support team can quote when a customer on Firefox ESR asks why the share button looks different.

Notice what the example did that a bullet list can’t: it attached a decision to every number. “Samsung Internet at 1.5%” isn’t trivia — it’s why that browser sits in Tier 2 instead of Tier 3. “4% of iOS users on 16.0–16.3” isn’t a footnote — it’s the argument for funding the push-notification fallback. Every cell in the matrix should survive that same “so what?” test; a row that doesn’t change a decision is decoration.

Keeping the Matrix Alive

A stale matrix is worse than none because it reads as current. The quarterly review is fifteen minutes:

  1. Check analytics — did any browser cross your usage threshold, in either direction?
  2. Check caniuse — did a ”❌” cell become supported? (Safari and Firefox ship constantly.)
  3. Update Browserslist — the last 2 versions baseline moves every 6 weeks whether you edit it or not.
  4. Re-run the smoke tests on Tier 2 browsers you haven’t touched.

The Browserslist config is the enforcement mechanism — it feeds Babel, Autoprefixer, and ESLint, so updating it changes what the build produces. Keep it as the single source of truth and treat the matrix document as its human-readable mirror.

Two edge cases the quarterly review should keep catching. Embedded webviews — links opened inside Facebook, Slack, or your own native app run in a webview that doesn’t match the surrounding browser’s version or feature set, and users stuck there report it as “your site is broken,” not “my webview is old.” If in-app traffic is meaningful, webviews deserve their own row. Locked-down fleets — enterprise customers on managed devices can’t upgrade even if they want to; a support drop for them is a contract conversation, not a banner.

The matrix also earns its keep outside engineering. Support quotes it when a user reports a glitch on an unsupported browser (“we don’t test that configuration — here’s how to upgrade”). Product uses it to answer “can we ship this feature?” before development starts. And leadership reads the tier table as the answer to “why are we spending CI minutes on Firefox ESR?” — the document converts invisible maintenance work into a visible policy.

Browser support decision flow — usage analytics assign each browser to a tier, the feature matrix flags gaps, gaps become polyfills or fallbacks, and the testing matrix plus quarterly review keep the baseline current

Variants

ContextApproachNotes
Enterprise appSupport older browsers (IE11 if required)May need heavier polyfills
Consumer appEvergreen only, latest 2 versionsMinimal polyfills
Mobile web appFocus on mobile browsersSafari iOS, Chrome Android, Samsung
Government siteStrict accessibility + legacy supportSection 508 may require older browser support
Internal toolChrome onlyMinimal cross-browser testing
Global audienceInclude regional browsers (UC, Opera Mini)Check regional usage stats

The variants mostly change which browsers appear, not how the matrix works. An internal tool collapses the whole thing to a one-line “Chrome latest” policy — and even that deserves a written line, because it’s the difference between a deliberate choice and an accident. A government or healthcare audience goes the other way: accessibility law and locked-down fleets can keep a legacy tier alive for years, which is exactly when the fallback column earns its space.

What Works

These habits come from teams that maintain a matrix rather than write it once and forget it. The common thread: the document only stays useful if the numbers inside it are measured, dated, and enforced by tooling — anything handwritten and unenforced drifts toward fiction within two quarters.

  1. Use Browserslist as the single source of truth — it feeds Babel, Autoprefixer, and ESLint
  2. Test on real browsers — emulators and simulators miss rendering differences
  3. Feature-detect, don’t browser-detect — if ('share' in navigator) not if (isSafari)
  4. Load polyfills dynamically — don’t penalize modern browsers with polyfill bytes
  5. Review the matrix quarterly — browser usage shifts, new versions drop
  6. Track browser usage analytics — drop support when usage falls below threshold
  7. Show upgrade banners for unsupported browsers — don’t silently break

Common Mistakes

  1. Supporting too many browsers — every browser adds testing and polyfill cost
  2. Not testing Safari — Safari has unique CSS and JS quirks despite being WebKit
  3. Browser detection instead of feature detection — fragile and breaks on new versions
  4. No fallback strategy — features that silently fail are worse than degraded experiences
  5. Ignoring mobile browsers — mobile usage often exceeds desktop
  6. No analytics to inform decisions — supporting browsers nobody uses wastes effort
  7. Not updating the matrix — browser versions move fast; stale matrices give false confidence

Troubleshooting

These are the failure modes the document itself runs into — most of them are process bugs, not browser bugs.

  • The matrix says “supported” but users report breakage: the matrix drifted from the Browserslist config — compare the document against .browserslistrc and rebuild. The document is a mirror; the config is the truth.
  • Polyfill bundle keeps growing: you’re shipping build-time polyfills for features only old browsers lack. Move them to dynamic import() behind a feature check so modern browsers skip the bytes.
  • Safari fails on a feature Chrome has: Safari adopts APIs later and with quirks (Push API needs 16.4+, File System Access is absent). Check caniuse before adopting, not after the bug report.
  • The upgrade banner never shows: the script targets an element that doesn’t exist or the probe is wrong — verify the id matches and test the probe in an old browser profile, not just in devtools’ UA override.
  • Usage numbers in the matrix look stale: the example percentages in this template are illustrative — replace them with your analytics; global averages differ wildly from, say, an enterprise audience locked to ESR builds.

Further Reading

Frequently Asked Questions

How do we decide which browsers to support?

Start with analytics data from your existing site. Identify browsers with > 0.5% usage. Check caniuse.com for feature compatibility. Consider your audience: enterprise users may be locked to older browsers. Set the baseline at the last 2 versions of each major browser plus any browser above your usage threshold.

Should we support Internet Explorer?

No. IE11 reached end of life in June 2022, and Microsoft redirects IE11 users to Edge. Supporting IE11 requires heavy polyfills, limits your CSS and JS feature set, and increases testing burden. Show an upgrade banner instead.

How do we handle Safari's slower feature adoption?

Safari adopts web features slower than Chrome and Firefox, and some APIs arrive a version or two behind (Push API needed 16.4). Test on Safari early, check caniuse before adopting a feature, and implement a fallback or dynamic polyfill for what Safari lacks. Don't assume "it works in Chrome so it works everywhere."

What is the cost of supporting an additional browser?

Each additional browser adds testing time (manual or automated), potential polyfill bundle size, debugging time for browser-specific bugs, and CSS compatibility work. Estimate 5-10% of frontend development time per additional browser tier. Weigh this against the user percentage that browser represents.

How do we know when to drop a browser?

When it falls below your usage threshold for two consecutive quarters — a single dip can be noise. Check both global stats and your own analytics; your audience may skew differently. Announce the drop in the matrix, bump the Browserslist config, and let the upgrade banner cover the stragglers.

How often should we review the matrix?

Quarterly at minimum. Browser versions update every 4-6 weeks and usage data shifts as users upgrade. Review the matrix, check analytics for usage changes, verify caniuse data for new features, and update the Browserslist configuration. Drop browsers that fall below the usage threshold.