Browser Compatibility

Difficulty: Advanced

Browser compatibility is the practice of ensuring your CSS works correctly across different browsers, versions, and devices. While modern browsers have converged on excellent standards support, differences still exist, especially for newer CSS features and on older browser versions that your users may still run. Understanding vendor prefixes, feature queries, progressive enhancement, and polyfill strategies is essential for building robust, production-quality web applications.

Vendor prefixes are browser-specific identifiers prepended to experimental or non-standard CSS properties. The main prefixes are -webkit- (Chrome, Safari, older Edge), -moz- (Firefox), -ms- (Internet Explorer, old Edge), and -o- (old Opera). During the 2010s, browsers used prefixes extensively to ship experimental features before standards were finalized. While modern browsers have largely moved away from requiring prefixes (using feature flags instead), some properties still need them for older browser versions. The -webkit- prefix remains relevant for some Safari-specific properties and older WebKit-based mobile browsers.

The @supports rule (also called a feature query) lets you conditionally apply CSS based on whether the browser supports a specific property-value pair. The syntax @supports (display: grid) { ... } applies the enclosed rules only in browsers that support CSS Grid. You can combine conditions with and, or, and not operators: @supports (display: grid) and (gap: 20px) { ... }. Feature queries are the CSS equivalent of JavaScript's feature detection and are essential for progressive enhancement, where you provide a baseline experience and enhance it for more capable browsers.

Progressive enhancement is the strategy of building a baseline experience that works in all browsers, then layering on advanced features for browsers that support them. The opposite approach, graceful degradation, starts with the full experience and then handles failures for older browsers. Progressive enhancement is generally preferred because it ensures all users get a functional experience. In CSS, this means writing base styles that work everywhere, then using @supports to add grid layouts, custom properties, backdrop-filter, and other modern features only where available.

Polyfills are JavaScript libraries that implement missing browser features. CSS polyfills are less common than JavaScript polyfills because CSS cannot be easily patched at runtime, but some exist: css-vars-ponyfill adds CSS custom property support to older browsers, and flexibility.js adds some Flexbox support to IE9. More commonly, you use PostCSS plugins at build time: Autoprefixer automatically adds vendor prefixes based on your browser support targets (from browserslist config), postcss-preset-env transforms modern CSS syntax into compatible equivalents, and postcss-custom-properties compiles CSS variables to static values for older browsers.

Modern browser compatibility tooling centers around the browserslist configuration. You define your target browsers (e.g., 'last 2 versions', '> 1%', 'not dead') in a .browserslistrc file or package.json. Tools like Autoprefixer, Babel, and postcss-preset-env read this config to determine what transformations are needed. The website caniuse.com provides detailed browser support tables for every CSS feature. The combination of caniuse.com for research, browserslist for configuration, Autoprefixer for prefixing, and @supports for runtime detection gives you a complete browser compatibility strategy.

Code examples

Vendor prefixes: manual and automated

/* Manual vendor prefixes (legacy approach) */
.box {
  -webkit-backdrop-filter: blur(10px);   /* Safari */
  backdrop-filter: blur(10px);           /* Standard */

  -webkit-user-select: none;   /* Safari, old Chrome */
  -moz-user-select: none;      /* Firefox */
  user-select: none;           /* Standard */

  -webkit-appearance: none;    /* Safari, Chrome */
  -moz-appearance: none;       /* Firefox */
  appearance: none;            /* Standard */
}

/* Flexbox with legacy prefixes (for old browsers) */
.flex-container {
  display: -webkit-box;      /* OLD: Safari 3-6, iOS Safari */
  display: -ms-flexbox;      /* TWEENER: IE 10 */
  display: flex;             /* STANDARD: Modern browsers */
}

/* Modern approach: let Autoprefixer handle it */
/* You write: */
.modern {
  display: flex;
  backdrop-filter: blur(10px);
  user-select: none;
}

/* Autoprefixer outputs (based on browserslist): */
.modern {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  -webkit-user-select: none;
  -moz-user-select: none;
  user-select: none;
}

/* browserslist config in package.json: */
/* "browserslist": [
     "last 2 versions",
     "> 1%",
     "not dead"
   ]
*/

Always write the standard property last so it overrides prefixed versions when supported. In modern workflows, Autoprefixer reads your browserslist targets and adds only the necessary prefixes automatically. You never write prefixes manually.

Feature queries with @supports

<style>
  /* Base layout: works everywhere (floats) */
  .card-grid {
    overflow: hidden; /* clearfix for floats */
  }
  .card {
    float: left;
    width: 30%;
    margin: 1.5%;
    background: #fff;
    padding: 16px;
    border: 1px solid #e5e7eb;
    border-radius: 8px;
  }

  /* Enhanced layout: CSS Grid (if supported) */
  @supports (display: grid) {
    .card-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
      gap: 20px;
      overflow: visible; /* undo clearfix */
    }
    .card {
      float: none;   /* undo float */
      width: auto;   /* undo fixed width */
      margin: 0;     /* grid gap handles spacing */
    }
  }

  /* Feature query with negation */
  @supports not (backdrop-filter: blur(10px)) {
    .modal-overlay {
      /* Fallback: solid semi-transparent background */
      background: rgba(0, 0, 0, 0.7);
    }
  }

  @supports (backdrop-filter: blur(10px)) {
    .modal-overlay {
      /* Enhanced: frosted glass effect */
      background: rgba(0, 0, 0, 0.3);
      backdrop-filter: blur(10px);
      -webkit-backdrop-filter: blur(10px);
    }
  }

  /* Combined conditions */
  @supports (display: grid) and (gap: 20px) {
    /* Only apply if BOTH grid and gap are supported */
    .advanced-grid {
      display: grid;
      gap: 20px;
    }
  }

  /* Check for selector support */
  @supports selector(:has(*)) {
    /* Only apply if :has() is supported */
    .form-group:has(input:invalid) {
      border-color: red;
    }
  }
</style>

@supports tests browser capabilities at runtime. The base styles use universally supported properties (floats). If grid is supported, the enhanced styles override the base. The 'not' operator provides fallbacks for unsupported features. The 'selector()' function tests support for CSS selectors like :has().

Progressive enhancement strategy

<style>
  /* LAYER 1: Universal base (works in every browser) */
  .container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 16px;
  }

  .card {
    background: #fff;
    border: 1px solid #ddd;
    padding: 16px;
    margin-bottom: 16px;
  }

  /* LAYER 2: Enhanced colors (CSS custom properties) */
  @supports (--custom: property) {
    :root {
      --bg-surface: #fff;
      --border-color: #e5e7eb;
      --text-primary: #111827;
      --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
    }
    .card {
      background: var(--bg-surface);
      border-color: var(--border-color);
      color: var(--text-primary);
      box-shadow: var(--shadow-sm);
    }
  }

  /* LAYER 3: Enhanced layout (Flexbox, supported since ~2015) */
  @supports (display: flex) {
    .card-actions {
      display: flex;
      gap: 8px;
      justify-content: flex-end;
    }
  }

  /* LAYER 4: Modern layout (Grid, supported since ~2017) */
  @supports (display: grid) {
    .card-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
      gap: 20px;
    }
  }

  /* LAYER 5: Cutting-edge features */
  @supports (container-type: inline-size) {
    .card-wrapper {
      container-type: inline-size;
    }
    @container (min-width: 400px) {
      .card {
        display: grid;
        grid-template-columns: 100px 1fr;
      }
    }
  }

  /* LAYER 6: Future/experimental */
  @supports selector(:has(*)) {
    .card:has(.card-image) {
      padding: 0;
    }
    .card:has(.card-image) .card-body {
      padding: 16px;
    }
  }
</style>

Progressive enhancement builds from a universally supported base. Each @supports layer adds capabilities for more modern browsers. Users on older browsers get a functional experience, while modern browser users get the enhanced version. No one gets a broken page.

Build tooling: Autoprefixer and PostCSS

/* postcss.config.js */
/*
module.exports = {
  plugins: [
    // Automatically adds vendor prefixes
    require('autoprefixer'),

    // Transforms modern CSS to compatible equivalents
    require('postcss-preset-env')({
      stage: 2,
      features: {
        'custom-properties': true,
        'nesting-rules': true,
        'custom-media-queries': true,
      },
    }),
  ],
};
*/

/* .browserslistrc (or in package.json) */
/*
last 2 versions
> 1%
not dead
not ie 11
*/

/* What you write (modern CSS): */
.card {
  display: flex;
  gap: 16px;
  backdrop-filter: blur(10px);
  user-select: none;

  & .title {
    font-size: 18px;
  }

  &:hover {
    transform: translateY(-2px);
  }
}

/* What Autoprefixer + PostCSS outputs: */
.card {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  gap: 16px;
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  -webkit-user-select: none;
  -moz-user-select: none;
  user-select: none;
}

.card .title {
  font-size: 18px;
}

.card:hover {
  -webkit-transform: translateY(-2px);
  transform: translateY(-2px);
}

/* Key tools:
   caniuse.com       - check feature support
   browserslist.dev  - preview target browsers
   autoprefixer      - add vendor prefixes
   postcss-preset-env - transpile modern CSS
*/

Build tools automate browser compatibility. Autoprefixer adds vendor prefixes based on browserslist targets. PostCSS preset-env transforms modern CSS (nesting, custom media) into compatible output. You write clean, modern CSS and the build pipeline ensures it works in your target browsers.

Key points

Concepts covered

Vendor Prefixes, Feature Queries, @supports, Progressive Enhancement, Graceful Degradation, Polyfills, Autoprefixer