Steven's Knowledge
Troubleshooting

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 support

Use 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

LayerToolWhat It Catches
SyntaxBrowserslist + Babel/SWCJS syntax not supported in targets
APIscore-js + polyfill.ioMissing standard library methods
CSSAutoprefixer + PostCSSMissing vendor prefixes and features
VisualBrowserStack / PlaywrightLayout and rendering differences
MobileChrome DevTools device modeTouch, viewport, safe area issues
A11yaxe-core / LighthouseAccessibility across platforms

On this page