Compatibility
Cross-browser compatibility issues, polyfills, and environment differences in frontend development
Compatibility Troubleshooting
Cross-browser and cross-environment compatibility remains a significant challenge in frontend development. This guide covers common compatibility issues and systematic solutions.
1. JavaScript API Not Available in Target Browsers
Problem
Using modern JavaScript features that aren't supported in all target browsers:
// structuredClone — not available in older browsers
const copy = structuredClone(complexObject);
// Array.at() — not available in Safari < 15.4
const lastItem = array.at(-1);
// AbortSignal.timeout — not available in many browsers
const signal = AbortSignal.timeout(5000);Solution
Define browser targets explicitly:
// package.json
{
"browserslist": [
">0.5%",
"last 2 versions",
"not dead",
"not ie 11"
]
}Use core-js for standard library polyfills:
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage', // Only polyfill what's needed
corejs: 3,
targets: '> 0.5%, last 2 versions, not dead',
}],
],
};Check compatibility before using features:
# CLI tool
npx browserslist # Show target browsers
npx caniuse-cli "css grid" # Check feature supportUse caniuse.com and MDN Browser Compatibility data as references.
2. CSS Features Not Supported Across Browsers
Problem
/* Container queries — not in older browsers */
@container (min-width: 400px) { }
/* :has() selector — not in Firefox < 121 */
.parent:has(.child) { }
/* Subgrid — not in all browsers */
grid-template-rows: subgrid;Solution
Progressive enhancement with @supports:
/* Base styles — works everywhere */
.grid {
display: flex;
flex-wrap: wrap;
}
/* Enhanced — only if supported */
@supports (display: grid) {
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}
}
@supports (container-type: inline-size) {
.card-container {
container-type: inline-size;
}
}PostCSS for automatic prefixing and fallbacks:
// postcss.config.js
module.exports = {
plugins: [
require('autoprefixer'), // Add vendor prefixes
require('postcss-preset-env')({
stage: 2,
features: {
'nesting-rules': true,
'custom-media-queries': true,
},
}),
],
};Provide fallback values:
.element {
/* Fallback for browsers without oklch support */
color: #3b82f6;
color: oklch(0.6 0.2 250);
/* Fallback for browsers without dvh */
height: 100vh;
height: 100dvh;
}3. Font Rendering Differences
Problem
- Fonts render differently across OS (Windows, macOS, Linux)
- Font weight appears different on Windows vs macOS
- Icon fonts show squares or missing glyphs
- Variable fonts not supported in older browsers
Solution
Consistent font stack with system fonts:
:root {
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji';
}Web font loading with fallback matching:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2-variations');
font-weight: 100 900;
font-display: swap;
}
/* Adjust fallback to match custom font metrics */
@font-face {
font-family: 'Inter-Fallback';
src: local('Arial');
size-adjust: 107%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
body {
font-family: 'Inter', 'Inter-Fallback', sans-serif;
}Optimize font-weight rendering:
/* Use -webkit-font-smoothing for consistency */
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}4. Mobile-Specific Issues
Problem
- 300ms tap delay on mobile browsers
- Viewport height changes when address bar shows/hides
- iOS Safari zoom on input focus
- Pull-to-refresh conflicts with scroll areas
- Safe area insets on notched devices
Solution
Viewport height — use dvh:
.full-height {
height: 100vh; /* Fallback */
height: 100dvh; /* Dynamic viewport height — adjusts for address bar */
}
/* Or use CSS env variables for safe areas */
.app {
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}Prevent iOS zoom on input focus:
<!-- Minimum font-size of 16px prevents zoom -->
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5" />/* Ensure inputs are at least 16px */
input, select, textarea {
font-size: max(16px, 1rem);
}Handle touch events properly:
/* Remove 300ms tap delay */
html {
touch-action: manipulation;
}
/* Disable pull-to-refresh in app areas */
.scroll-container {
overscroll-behavior: contain;
}5. Third-Party Script Compatibility
Problem
- Third-party scripts block page rendering
- Analytics scripts conflict with SPA routing
- Payment widgets break after framework updates
- CORS errors when embedding external content
Solution
Load third-party scripts non-blocking:
<!-- Defer — downloads during parse, executes after DOM ready -->
<script src="https://analytics.example.com/script.js" defer></script>
<!-- Async — downloads and executes independently -->
<script src="https://ads.example.com/ad.js" async></script>Isolate third-party code:
// Load in useEffect to avoid SSR issues
useEffect(() => {
const script = document.createElement('script');
script.src = 'https://third-party.com/widget.js';
script.async = true;
script.onload = () => {
// Initialize after load
window.ThirdPartyWidget?.init({ containerId: 'widget' });
};
document.body.appendChild(script);
return () => {
document.body.removeChild(script);
window.ThirdPartyWidget?.destroy();
};
}, []);Web Workers for heavy third-party processing:
// Offload analytics to a Web Worker via Partytown
// next.config.js
const { withPartytown } = require('@builder.io/partytown/next');
module.exports = withPartytown({
partytown: {
forward: ['dataLayer.push', 'gtag'],
},
});6. Internationalization (i18n) Issues
Problem
- Text overflows when translated to longer languages (German, Finnish)
- Right-to-left (RTL) layout breaks
- Date/number formatting inconsistencies
- Unicode characters cause layout issues
Solution
Design for text expansion (translations can be 2-3x longer):
/* Use flexible layouts that accommodate text expansion */
.button {
padding: 8px 16px;
white-space: nowrap; /* Or allow wrapping if appropriate */
min-width: min-content;
}
/* Use logical properties for RTL support */
.card {
margin-inline-start: 16px; /* Instead of margin-left */
padding-inline-end: 8px; /* Instead of padding-right */
border-inline-start: 2px solid blue; /* Instead of border-left */
}
/* Direction-aware layouts */
html[dir="rtl"] .icon-before::before {
/* Flip icons in RTL */
transform: scaleX(-1);
}Use Intl APIs for formatting:
// Number formatting
const formatter = new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
});
formatter.format(1234.56); // "1.234,56 €"
// Date formatting
const dateFormatter = new Intl.DateTimeFormat('ja-JP', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
dateFormatter.format(new Date()); // "2025年3月5日"
// Relative time
const rtf = new Intl.RelativeTimeFormat('zh-CN', { numeric: 'auto' });
rtf.format(-1, 'day'); // "昨天"7. Progressive Web App (PWA) Compatibility
Problem
- PWA install prompt doesn't appear
- Offline mode doesn't work reliably
- Push notifications not supported on iOS
- App behavior differs between "installed" and "in browser"
Solution
Correct Web App Manifest:
{
"name": "My Application",
"short_name": "MyApp",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3b82f6",
"icons": [
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "/icon-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
]
}Reliable offline with Workbox:
// sw.ts — Service Worker with Workbox
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate, CacheFirst } from 'workbox-strategies';
// Precache app shell
precacheAndRoute(self.__WB_MANIFEST);
// Cache API responses
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new StaleWhileRevalidate({ cacheName: 'api-cache' })
);
// Cache static assets
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({ cacheName: 'image-cache', plugins: [/* expiration */] })
);Summary: Compatibility Testing Strategy
| Layer | Tool | What It Catches |
|---|---|---|
| Syntax | Browserslist + Babel/SWC | JS syntax not supported in targets |
| APIs | core-js + polyfill.io | Missing standard library methods |
| CSS | Autoprefixer + PostCSS | Missing vendor prefixes and features |
| Visual | BrowserStack / Playwright | Layout and rendering differences |
| Mobile | Chrome DevTools device mode | Touch, viewport, safe area issues |
| A11y | axe-core / Lighthouse | Accessibility across platforms |