Browser Compatibility Testing Matrix: What to Test and Why
Most browser compatibility guides tell you to test on Chrome, Firefox, Safari, and Edge. That's a reasonable starting point, but it's not a strategy. A well-designed browser test matrix is based on your actual user analytics, the CSS and JavaScript features your app depends on, and the risk of breaking specific browser/OS combinations.
This guide covers how to build a realistic, maintainable browser compatibility matrix and automate testing against it.
Start With Real Usage Data
Before defining your matrix, pull browser distribution data from your analytics. The numbers vary dramatically by audience:
- Developer tools / B2B SaaS: Chrome 60-70%, Firefox 15%, Safari 10%, Edge 10%. Firefox matters more than average; IE is irrelevant.
- Consumer apps (US/EU): Safari/iOS 35-45%, Chrome 40-50%, Chrome/Android 20-30%. Safari matters enormously.
- Enterprise software: Edge 25-30%, Chrome 45%, older browser versions still in use.
- Southeast Asia / emerging markets: Samsung Internet 15-20%, older Chrome versions on Android common.
Your real data is the only correct answer. Check Google Analytics, Plausible, or whatever you use. Export browser distribution for the last 90 days and sort by usage percentage.
Building Your Matrix
A practical browser matrix has three tiers:
Tier 1: Must Pass (run on every PR)
- Browsers representing >10% of your traffic
- Your primary development browser
- The latest stable version
Tier 2: Should Pass (run nightly or pre-release)
- Browsers representing 2-10% of your traffic
- N-1 versions of Tier 1 browsers (previous major version)
Tier 3: Nice to Have (run quarterly or on demand)
- Browsers representing <2% of your traffic
- Legacy versions you're considering dropping
- Niche browsers you've received specific bug reports about
Example matrix for a typical B2B SaaS:
| Browser | Version | OS | Tier | % Traffic |
|---|---|---|---|---|
| Chrome | Latest | Windows 11 | 1 | 45% |
| Safari | Latest | macOS | 1 | 15% |
| Chrome | Latest | macOS | 1 | 12% |
| Edge | Latest | Windows 11 | 1 | 10% |
| Firefox | Latest | Windows | 2 | 8% |
| Chrome | Latest | Android | 2 | 5% |
| Safari | iOS 16/17 | iPhone | 2 | 4% |
| Chrome | N-1 | Windows | 3 | 1% |
Automating the Matrix with Playwright
Configure Playwright projects to match your matrix:
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
// Tier 1 — run on every PR
{
name: 'Chrome-Windows',
use: {
...devices['Desktop Chrome'],
channel: 'chrome', // Use system Chrome, not bundled Chromium
},
testMatch: ['**/*.spec.ts'],
},
{
name: 'Safari-macOS',
use: { ...devices['Desktop Safari'] },
testMatch: ['**/*.spec.ts'],
},
{
name: 'Edge-Windows',
use: {
...devices['Desktop Edge'],
channel: 'msedge',
},
testMatch: ['**/*.spec.ts'],
},
// Tier 2 — nightly
{
name: 'Firefox-Windows',
use: { ...devices['Desktop Firefox'] },
testMatch: ['**/*.spec.ts'],
},
{
name: 'Chrome-Android',
use: { ...devices['Pixel 7'] },
testMatch: ['**/*.spec.ts'],
},
{
name: 'Safari-iOS',
use: { ...devices['iPhone 14'] },
testMatch: ['**/*.spec.ts'],
},
],
});Run tiers separately:
# Tier 1 (PR checks)
npx playwright <span class="hljs-built_in">test --project=Chrome-Windows --project=Safari-macOS --project=Edge-Windows
<span class="hljs-comment"># Tier 1 + 2 (nightly)
npx playwright <span class="hljs-built_in">testFeature-Based Compatibility Testing
Beyond running your full test suite across browsers, add specific tests for browser compatibility of features you use:
// tests/compat/css-features.spec.ts
test.describe('CSS feature compatibility', () => {
test('CSS Grid renders layout correctly', async ({ page }) => {
await page.goto('/dashboard');
const gridContainer = page.locator('[data-testid="metrics-grid"]');
await expect(gridContainer).toBeVisible();
// Verify grid layout actually applied
const gridStyle = await gridContainer.evaluate(el =>
window.getComputedStyle(el).display
);
expect(gridStyle).toBe('grid');
// Check columns are rendered (4-column layout)
const gridColumns = await gridContainer.evaluate(el =>
window.getComputedStyle(el).gridTemplateColumns
);
expect(gridColumns.split(' ')).toHaveLength(4);
});
test('CSS custom properties (variables) work', async ({ page }) => {
await page.goto('/');
const primaryButton = page.locator('[data-testid="cta-button"]');
const backgroundColor = await primaryButton.evaluate(el =>
window.getComputedStyle(el).backgroundColor
);
// Should not be empty or inherit — variable should have resolved
expect(backgroundColor).not.toBe('rgba(0, 0, 0, 0)');
expect(backgroundColor).not.toBe('');
});
test('CSS container queries work', async ({ page }) => {
// Only run this test if your app uses container queries
await page.goto('/');
// Resize to trigger container query
await page.setViewportSize({ width: 800, height: 600 });
const card = page.locator('[data-testid="responsive-card"]');
const layout = await card.evaluate(el => el.getAttribute('data-layout'));
expect(layout).toBe('compact'); // Container query should set data-layout
});
});// tests/compat/js-features.spec.ts
test.describe('JavaScript API compatibility', () => {
test('Intersection Observer works', async ({ page }) => {
await page.goto('/features');
// Scroll to trigger lazy loading
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(500);
// Lazy-loaded images should now be visible
const lazyImages = await page.locator('img[loading="lazy"]').all();
for (const img of lazyImages.slice(0, 3)) {
await expect(img).toBeVisible();
}
});
test('Clipboard API write works (with permission)', async ({ page, context }) => {
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
await page.goto('/docs/api-reference');
await page.click('[data-testid="copy-code-button"]');
const clipboardContent = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardContent.length).toBeGreaterThan(0);
});
test('Web Share API is handled when unavailable', async ({ page }) => {
// WebKit on desktop doesn't support Web Share — verify graceful fallback
await page.goto('/blog/post/1');
// Share button should exist
const shareButton = page.locator('[data-testid="share-button"]');
await expect(shareButton).toBeVisible();
// Click it — should either open Web Share or show fallback
await shareButton.click();
// Either share dialog or fallback copy-link modal should appear
const shareDialog = page.locator('[data-testid="share-dialog"], [data-testid="copy-link-modal"]');
await expect(shareDialog).toBeVisible();
});
});Handling Known Browser Differences
Some behaviors legitimately differ between browsers. Handle them with conditional assertions rather than separate tests:
test('date input formats correctly', async ({ page, browserName }) => {
await page.goto('/events/create');
const dateInput = page.locator('[data-testid="event-date"]');
await dateInput.fill('2024-12-25');
const inputValue = await dateInput.inputValue();
// Firefox and WebKit may format dates differently
if (browserName === 'webkit') {
// Safari may format as 12/25/2024 on US locale
expect(inputValue).toMatch(/12\/25\/2024|2024-12-25/);
} else {
expect(inputValue).toBe('2024-12-25');
}
});Maintaining the Matrix Over Time
Browser compatibility matrices require maintenance as browser usage changes and new browser versions ship:
Quarterly review tasks:
- Pull updated browser analytics — have any browsers crossed the 10% threshold (tier 1) or dropped below 2% (tier 3)?
- Update browser version targets in your Playwright config
- Remove browsers from the matrix that have dropped off entirely
- Check for new CSS/JS features you're using that have compatibility implications
When to drop a browser:
- Traffic drops below 1% of your audience
- The browser vendor has ended support (IE 11 support ended June 2022)
- Supporting it requires maintaining incompatible code paths that slow development
When to add a browser:
- Traffic exceeds 5% (add to Tier 2)
- You receive multiple bug reports about a specific browser
- You're entering a new market where a specific browser is dominant
Caniuse Integration
For CSS/JS features, use caniuse.com data to know before coding whether a feature needs a fallback:
// CI check — flag if any CSS features are used that don't support your matrix
// This can be integrated as an eslint plugin or build step:
// browserslist (.browserslistrc):
// > 1% in US
// last 2 Chrome versions
// last 2 Firefox versions
// last 2 Safari versions
// last 2 Edge versions
// Then use postcss-preset-env or babel to auto-add prefixes/fallbacks
// based on your actual browser targetsAutomating Cross-Browser Failure Analysis
When cross-browser tests fail, you need to know quickly which browsers are affected and whether it's a real bug or flakiness:
// Parse test results to show browser-specific failures
// playwright-report/index.html groups by project (browser) automatically
// CLI command to see failures by browser
npx playwright test --reporter=list | grep FAILEDFor systematic analysis, use Playwright's JSON reporter and parse the results:
npx playwright test --reporter=json > results.json
<span class="hljs-built_in">cat results.json <span class="hljs-pipe">| jq <span class="hljs-string">'
.suites[].suites[] |
select(.specs[].tests[].results[].status == "failed") <span class="hljs-pipe">|
{browser: .title, test: .specs[].title}
'The Minimum Viable Browser Matrix
If you're just getting started with cross-browser testing and need a starting point before you have analytics data:
| Browser | Why |
|---|---|
| Chrome (latest) | ~65% of global web traffic |
| Safari/WebKit | ~19% globally, renders differently, iOS requirement |
| Firefox (latest) | ~4% globally, Gecko engine reveals real compatibility issues |
These three cover Chromium (Chrome/Edge/Opera), WebKit (Safari/iOS), and Gecko (Firefox) — the three distinct browser engines. Testing all three catches engine-level compatibility issues. Edge and Chrome share the Chromium engine, so an additional Edge project is only necessary if you have significant Edge traffic or need to test Edge-specific features.
A well-defined, analytics-driven browser matrix is the difference between cross-browser testing that's comprehensive and cross-browser testing that's expensive theater. Test the browsers your users actually use, at a depth proportional to that usage, and automate the matrix maintenance so it stays current as browser usage evolves.