Browser Compatibility
Date: 2026-08-17
Deciding what you can use, from evidence rather than habit. The two skills are reading support data against your own traffic rather than global averages, and detecting features rather than browsers — because the browser-sniffing approach has been wrong for twenty years and still gets written.
Browser compatibility is whether a feature works as intended across the browsers and versions your users actually run.
Feature detection, not browser detection
// ✗ browser detection. wrong the moment a string changes, and the
// user agent is deliberately being reduced anyway
if (navigator.userAgent.includes('Safari')) { useFallback(); }
// ✓ detect the thing you actually need
if ('IntersectionObserver' in window) { observe(); } else { fallback(); }
if (CSS.supports('container-type: inline-size')) { … }/* the CSS version — no JavaScript needed */
@supports (view-transition-name: none) { … }
/* and the implicit one: an unsupported property is simply ignored */
.card { display: flex; display: grid; } /* grid wins where supported */User agent strings are being frozen and reduced as an anti-fingerprinting measure, so parsing them is a decaying strategy on top of being a wrong one — Fingerprinting, Browser Privacy Restrictions.
Reading support data properly
Global usage figures are not your traffic, and the gap decides real questions.
a feature at 91% global support
your traffic the decision
UK retail, 62% mobile check YOUR Safari and Android
Safari 34%, Chrome 41%, versions specifically — if the
Firefox 4%, other 21% unsupported 9% is concentrated in
an old Safari you have a lot of,
that's 6% of YOUR revenue
internal admin tool, 91% is irrelevant. you support
Chrome on managed laptops one browser and it's current
Get the real distribution from your own analytics, segmented by revenue rather than sessions. Old browsers skew towards older devices, which skew towards particular customer segments — and on some retailers those segments spend more, not less — Segmentation (analysis).
Baseline is the useful shared vocabulary: a feature is “newly available” once it’s in all major engines, and “widely available” after roughly 30 months of that. It’s a much better basis for a team convention than “is it on caniuse”.
Setting a support target
Write it down once, as a policy, rather than arguing per feature:
TIERED SUPPORT
FULL current and previous version of each major browser
→ every feature, full experience
FUNCTIONAL anything back to [a stated date or version]
→ can browse, add to basket, check out
→ may lack animations, container queries, niceties
UNSUPPORTED below that
→ served a working baseline, no guarantees
→ NOT blocked. never block
Never block an unsupported browser. A “please upgrade” page on a corporate machine the user can’t upgrade is a lost sale for no benefit — and it’s frequently shown to a modern browser that was misidentified.
Tie the policy to a number. “We support browsers covering 98% of our revenue, reviewed each quarter” is a decision that can be revisited with evidence, rather than a preference.
Polyfills, transpilation and their costs
POLYFILL adds a missing API at runtime
costs bytes for EVERYONE unless conditionally loaded
TRANSPILE rewrites modern syntax to older syntax at build time
costs bytes and often produces slower code
— Transpilation
PONYFILL a standalone implementation you import explicitly,
rather than patching globals. safer, no global side effects
A fourth term belongs beside these and isn’t in the table because it answers a different question: a shim wraps an API that is present but doesn’t behave as you need. Only polyfills are ever deletable — Shims and Polyfills.
The browserslist target is the single highest-value line to check. A stale one silently transpiles and polyfills for browsers nobody uses, and it’s routinely worth 40–60 KB of a bundle — Bundle Analysis.
Differential serving — a modern bundle for modern browsers, a legacy one for the rest — removes the cost for the majority:
<script type="module" src="/app.modern.js"></script>
<script nomodule src="/app.legacy.js"></script>Modern browsers ignore nomodule; old ones don’t understand type="module" and skip it. Two bundles, each served to the right audience, no user agent parsing.
Testing it
- Real devices for the important ones. Emulators get layout roughly right and input, scrolling, and Safari’s quirks wrong. One cheap Android and one older iPhone cover most of what breaks
- A device lab service for the long tail, wired into CI for the critical journeys — End-to-End Testing
- Test the functional tier deliberately, not just the full one. Nobody checks whether checkout works on the browsers they claim to support functionally, which is exactly where the revenue risk sits
- Watch Error Tracking segmented by browser. A spike concentrated in one version is the earliest signal that a feature you shipped isn’t as supported as you thought
The pragmatic position
Use modern features and enhance progressively. Waiting for universal support means never using anything, and the cost of a fallback is usually one @supports block or one feature check.
✓ use it, with a working baseline underneath — Progressive Enhancement
✓ use it where the fallback is "slightly less nice"
✗ use it where the fallback is "checkout doesn't work"
The line is the commercial path. Anything on browse → basket → checkout gets a tested fallback; anything else can degrade to nothing much.
Where it interacts
- Progressive Enhancement — the strategy that makes most compatibility questions cheap to answer
- Transpilation — the build-time half, and where the browserslist target does its work
- Graceful Degradation — runtime failure rather than capability, and often confused with this
- Email Client Rendering — the same problem in a far less forgiving runtime