MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Web Platform Interop 2025: Cross-Browser Compatibility

Major interoperability achievements: CSS features, APIs, and the Interop project.

Web PlatformInteropBrowserStandards

By MinhVo

Introduction

The Interop project is a collaboration between browser vendors (Google, Apple, Microsoft, Mozilla) and other stakeholders to improve cross-browser compatibility. Each year, they select focus areas where browsers should align their implementations. The 2025 focus areas represent the most ambitious effort yet, targeting features that will make the web platform more consistent and capable.

This guide covers the Interop 2025 focus areas, what they mean for developers, and how to use the newly interoperable features.

Cross-browser compatibility

What is Interop?

Interop is an annual initiative where browser vendors agree on a set of web platform features to focus on improving. Each focus area has specific tests that all browsers must pass. The goal is to eliminate cross-browser inconsistencies that frustrate developers and fragment the web.

The project traces its origins to the Compat effort started in 2021, when engineers from Google, Microsoft, Mozilla, Apple, Bocoup, and Igalia first formalized a collaborative process for identifying and resolving cross-browser discrepancies. Each year the scope has expanded — from 15 focus areas in 2022 to over 25 in 2025 — and the scores have steadily improved, demonstrating that targeted collaboration between vendors produces tangible results for developers.

The Interop project tracks progress with a public dashboard showing each browser's score across all focus areas. Scores are calculated based on automated test results from the Web Platform Tests (WPT) suite, a massive collection of over 1.7 million tests maintained by the broader web community. Each focus area has a defined set of WPT tests, and a browser's score for that area reflects the percentage of those tests it passes.

How Focus Areas Are Selected

The selection process begins with a public proposal period where developers, organizations, and browser vendors submit candidates. Proposals are evaluated against several criteria:

  • Developer demand: Survey data, Stack Overflow questions, and GitHub issues quantify how many developers struggle with a particular inconsistency.
  • Web usage data: HTTP Archive data shows how frequently a feature is used across the web, which helps estimate the user impact of improved interoperability.
  • Test coverage: Proposals with existing WPT tests are preferred, though new tests can be written as part of the project.
  • Specification maturity: Features with stable specifications are more likely to produce consistent implementations than those still in active development.
  • Vendor commitment: Each participating vendor must commit engineering resources to implement or fix the specified features.

After proposals are submitted, a committee of browser vendors and advocacy organizations reviews them, negotiates priorities, and publishes the final list of focus areas. This typically happens in January or February, with progress tracked throughout the year.

Interop 2025 Focus Areas

CSS Anchor Positioning

CSS Anchor Positioning lets you position elements relative to other elements (anchors) without JavaScript. This is one of the most impactful additions to CSS in recent years, replacing thousands of lines of custom JavaScript positioning logic for tooltips, popovers, dropdown menus, and contextual menus.

The core concepts are anchor names, position anchors, and anchor functions. An anchor element is declared with anchor-name, and a positioned element references it with position-anchor. The anchor() function then resolves to the anchor element's edge coordinates.

/* Define an anchor */
.trigger {
  anchor-name: --trigger;
}
 
/* Position relative to the anchor */
.tooltip {
  position: fixed;
  position-anchor: --trigger;
  top: anchor(bottom);
  left: anchor(center);
  translate: -50% 8px;
}
 
/* Fallback positioning */
.tooltip {
  position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
}
 
/* Position using inset-area shorthand */
.dropdown {
  position: fixed;
  position-anchor: --menu-button;
  inset-area: block-end span-inline-end;
}

One of the most powerful aspects of anchor positioning is position-try-fallbacks, which lets you define a cascade of fallback positions the browser will attempt when the primary position would cause the element to overflow its containing block. This eliminates the need for JavaScript-based flip logic that libraries like Popper.js and Floating UI previously provided.

The inset-area property provides a convenient shorthand for common positioning patterns. Instead of specifying individual top, left, bottom, and right values, you can describe the relationship between the anchor and the positioned element using logical property names. For example, block-end span-inline-end means "below the anchor, spanning to the right."

/* Complex positioning with inset-area */
/* Position a menu below and aligned to the start of the trigger */
.menu {
  position: fixed;
  position-anchor: --trigger;
  inset-area: block-end inline-start;
}
 
/* Position a popover above the anchor with fallback to below */
.popover {
  position: fixed;
  position-anchor: --info-icon;
  inset-area: block-start;
  position-try-fallbacks: flip-block;
}
 
/* Use anchor() for precise coordinate-based positioning */
.callout {
  position: fixed;
  position-anchor: --chart-point;
  top: anchor(--chart-point top);
  left: calc(anchor(--chart-point right) + 8px);
}

The interop effort for anchor positioning focused on ensuring that all browsers handle position-try-fallbacks consistently, including the priority ordering of fallback positions and the behavior when multiple fallback positions would still result in overflow. Before Interop 2025, Safari and Firefox had notable differences in how they evaluated fallback positions, leading to tooltips that would flip inconsistently across browsers.

Scroll-Driven Animations

Animations driven by scroll position instead of time. Scroll-driven animations tie CSS animation progress to the scroll position of a scroll container, enabling parallax effects, scroll progress indicators, and entrance animations without any JavaScript.

There are two timeline types: scroll() for tracking the scroll progress of an ancestor scroller, and view() for tracking an element's visibility within a scroll port. Each provides a block or inline axis option.

/* Animate based on scroll position */
.progress-bar {
  animation: grow-width linear;
  animation-timeline: scroll();
}
 
@keyframes grow-width {
  from { width: 0; }
  to { width: 100%; }
}
 
/* Animate elements as they scroll through the viewport */
.fade-in {
  animation: fade-in linear;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}
 
@keyframes fade-in {
  from { opacity: 0; transform: translateY(20px); }
  to { opacity: 1; transform: translateY(0); }
}

The view() timeline is particularly powerful because it automatically tracks the element's position relative to its scroll port. The animation-range property lets you control exactly when the animation starts and ends relative to the element's visibility. Common range values include entry (when the element enters the scroll port), exit (when it leaves), contain (when it is fully within the port), and cover (the entire range where the element overlaps the port).

/* Staggered entrance animations for a list */
.list-item {
  animation: slide-up ease-out both;
  animation-timeline: view();
  animation-range: entry 0% entry 40%;
}
 
@keyframes slide-up {
  from {
    opacity: 0;
    translate: 0 30px;
  }
  to {
    opacity: 1;
    translate: 0 0;
  }
}
 
/* Parallax background effect */
.hero-bg {
  animation: parallax-scroll linear;
  animation-timeline: scroll(root);
  animation-range: 0% 100%;
}
 
@keyframes parallax-scroll {
  from { transform: translateY(0); }
  to { transform: translateY(-30%); }
}

The Interop 2025 effort for scroll-driven animations focused on two main areas: correct computation of view() timeline ranges across different writing modes and scroll container configurations, and consistent behavior when animations have animation-range values that cause them to start or end mid-progress. Before Interop, Firefox had issues with view() timeline calculations for elements inside nested scroll containers, and Safari did not yet support animation-range with view() timelines.

View Transitions Level 2

Cross-document view transitions for multi-page applications. While Level 1 of the View Transitions API only supported same-document transitions (within a single-page application), Level 2 extends the API to support transitions between different documents during navigation.

/* Enable cross-document view transitions */
@view-transition {
  navigation: auto;
}
 
/* Customize transition animations */
::view-transition-old(root) {
  animation: fade-out 0.3s ease-out;
}
 
::view-transition-new(root) {
  animation: fade-in 0.3s ease-in;
}

The navigation: auto directive tells the browser to capture the current page state before navigation and the new page state after navigation, then cross-fade between them. You can customize which elements participate in the transition by assigning view-transition-name to specific elements on both pages.

/* Transition a hero image between pages */
.hero-image {
  view-transition-name: hero;
}
 
/* Define custom animations for named transitions */
::view-transition-old(hero) {
  animation: scale-down 0.4s ease-in-out;
}
 
::view-transition-new(hero) {
  animation: scale-up 0.4s ease-in-out;
}
 
@keyframes scale-down {
  from { transform: scale(1); opacity: 1; }
  to { transform: scale(0.8); opacity: 0; }
}
 
@keyframes scale-up {
  from { transform: scale(1.2); opacity: 0; }
  to { transform: scale(1); opacity: 1; }
}

The Interop effort for View Transitions Level 2 focused on cross-document transition behavior when pages are served from different origins, handling of transition animations when the old and new pages have different viewport sizes, and consistent behavior for named transitions that exist on one page but not the other.

Invoker Commands

Declarative command invocation without JavaScript. Invoker Commands allow HTML elements to trigger actions on other elements using the commandfor and command attributes. This reduces the amount of JavaScript needed for common UI patterns.

<!-- Toggle a dialog -->
<button commandfor="my-dialog" command="show-modal">Open Dialog</button>
<dialog id="my-dialog">
  <p>Dialog content</p>
  <button commandfor="my-dialog" command="close">Close</button>
</dialog>
 
<!-- Toggle visibility -->
<button commandfor="details" command="toggle-open">Toggle</button>
<div id="details">Hidden content</div>

The commandfor attribute specifies the target element by ID, and the command attribute specifies the action to invoke. Built-in commands include show-modal and close for dialog elements, show-popover, hide-popover, and toggle-popover for popover elements, and toggle-open for details elements. The API is extensible — custom commands can be handled with the command event.

<!-- Custom command handling -->
<button commandfor="player" command="play-pause">Play/Pause</button>
<video id="player" src="video.mp4"></video>
 
<script>
  player.addEventListener('command', (event) => {
    if (event.command === 'play-pause') {
      player.paused ? player.play() : player.pause();
    }
  });
</script>

The Interop 2025 focus on Invoker Commands ensured that all browsers fire the command event consistently, handle commandfor targeting across shadow DOM boundaries, and support the same set of built-in commands.

Speculation Rules API

Tell the browser which pages to prefetch or prerender. The Speculation Rules API replaces the older <link rel="prefetch"> and <link rel="prerender"> hints with a structured JSON format that supports more sophisticated targeting and eagerness levels.

<script type="speculationrules">
{
  "prerender": [
    {
      "where": {
        "href_matches": "/products/*"
      },
      "eagerness": "moderate"
    }
  ],
  "prefetch": [
    {
      "where": {
        "href_matches": "/blog/*"
      },
      "eagerness": "conservative"
    }
  ]
}
</script>

There are four eagerness levels: immediate (trigger on page load), eager (trigger as soon as possible), moderate (trigger on hover or pointer proximity), and conservative (trigger only on explicit user interaction like click). The where clause supports URL matching patterns and selector-based targeting.

<script type="speculationrules">
{
  "prerender": [
    {
      "urls": ["/checkout", "/account"]
    },
    {
      "where": {
        "and": [
          { "href_matches": "/product/*" },
          { "not": { "href_matches": "/product/out-of-stock*" } }
        ]
      },
      "eagerness": "conservative"
    }
  ]
}
</script>

The Interop effort focused on consistent handling of speculation rules across browsers, including proper resource management when multiple speculation rules target the same URL, and consistent behavior when prerendered pages modify shared state like cookies or IndexedDB.

Dialog toggle and customize

Enhanced <dialog> element with better customization and new events. The dialog element now supports the beforetoggle and toggle events for modal and non-modal dialogs, and ::backdrop styling works consistently across browsers.

/* Customize dialog backdrop */
dialog::backdrop {
  background: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(4px);
}
 
/* Open state animation */
dialog[open] {
  animation: dialog-in 0.2s ease-out;
}
 
dialog:not([open]) {
  animation: dialog-out 0.2s ease-in;
}
 
@keyframes dialog-in {
  from { opacity: 0; transform: scale(0.95); }
  to { opacity: 1; transform: scale(1); }
}

The Interop focus area ensured that ::backdrop pseudo-element rendering, focus trapping behavior, and the cancel and close events fire consistently across all browsers.

@starting-style

Define styles for when an element is first rendered or transitions from display: none. The @starting-style at-rule defines the initial values for CSS properties when an element first appears in the DOM or transitions from display: none to a visible state. This is essential for entry animations on elements like popovers and dialogs.

/* Animate elements on first render */
.dialog[open] {
  opacity: 1;
  transform: scale(1);
  transition: opacity 0.2s, transform 0.2s;
}
 
@starting-style {
  .dialog[open] {
    opacity: 0;
    transform: scale(0.9);
  }
}
 
/* Animate popovers */
[popover]:popover-open {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.2s, transform 0.2s, display 0.2s allow-discrete;
}
 
@starting-style {
  [popover]:popover-open {
    opacity: 0;
    transform: translateY(-10px);
  }
}

Without @starting-style, transitions from display: none to visible would happen instantly because the browser would have no "from" state to animate from. The Interop effort ensured that all browsers correctly evaluate @starting-style rules and compute the initial style for transitions.

Exclusive Accordion

Native exclusive accordion behavior without JavaScript. The name attribute on <details> elements creates an accordion group where only one details element can be open at a time. When one element is opened, all others in the same group automatically close.

<details name="faq">
  <summary>What is web development?</summary>
  <p>Web development is the process of building websites and web applications.</p>
</details>
 
<details name="faq">
  <summary>What is CSS?</summary>
  <p>CSS is a style sheet language for describing the presentation of web pages.</p>
</details>
 
<details name="faq">
  <summary>What is JavaScript?</summary>
  <p>JavaScript is a programming language for the web.</p>
</details>

When one <details> is opened, others with the same name attribute automatically close. The name attribute scopes the exclusive behavior — you can have multiple accordion groups on the same page by using different names. The Interop focus ensured that all browsers close previously-opened details elements before opening the new one, fire the toggle event in the correct order, and correctly handle the case where multiple details elements share the same name but are in different shadow DOM scopes.

:user-valid and :user-invalid

Style form elements based on user interaction, not just validity state. The :user-valid and :user-invalid pseudo-classes are different from :valid and :invalid because they only apply after the user has interacted with the input. This prevents the common problem of showing validation errors before the user has even started filling out a form.

/* Only show validation styles after user interaction */
input:user-valid {
  border-color: green;
}
 
input:user-invalid {
  border-color: red;
  box-shadow: 0 0 0 2px rgba(255, 0, 0, 0.2);
}
 
/* Different from :valid/:invalid which apply immediately */
input:valid {
  /* Always applied when valid — shows green border on page load */
}
 
/* Combine with :focus for contextual feedback */
input:user-invalid:focus {
  border-color: red;
  outline-color: red;
}
 
input:user-valid:focus {
  border-color: green;
  outline-color: green;
}

The Interop effort focused on ensuring consistent definition of what constitutes "user interaction" across browsers. This includes typing in an input, selecting an option in a select element, changing a checkbox or radio button, and blurring a field after modification. The behavior differs subtly from the :focus-visible pseudo-class, and the Interop project ensured all browsers agree on when each pseudo-class applies.

Browser standards

The Baseline Concept

The Baseline initiative by the web standards community defines which web platform features are available across all major browsers. A feature that reaches Baseline Widely Available status can be used in production without polyfills or feature detection. This gives developers a clear signal about which features are safe to adopt.

There are two Baseline tiers. Baseline Newly Available means a feature is supported in the latest version of all major browsers. Baseline Widely Available means the feature has been newly available for at least 30 months, which typically corresponds to broad user coverage as older browser versions fall out of use.

The web-features npm package provides programmatic access to Baseline data. You can use it in build scripts, linters, or documentation generators to check whether features your code depends on are Baseline.

// Check Baseline status programmatically
import { getStatus, features } from 'web-features';
 
const anchorPositioning = features['anchor-positioning'];
console.log(anchorPositioning.status.baseline); // 'high' or 'low' or false
console.log(anchorPositioning.status.baseline_high_date); // When it became Widely Available

Tools like eslint-plugin-baseline can lint your codebase to identify usage of non-Baseline features, and postcss-preset-env can automatically add fallbacks for features that haven't reached Baseline yet.

Step-by-Step Adoption

Step 1: Check Browser Support

Before using any new Interop feature, verify support with feature detection. CSS features can be detected with @supports, and JavaScript APIs can be checked with the in operator or by testing for API existence.

// Feature detection for CSS Anchor Positioning
if (CSS.supports('anchor-name', '--test')) {
  // Use anchor positioning
}
 
// Feature detection for scroll-driven animations
if (CSS.supports('animation-timeline', 'scroll()')) {
  // Use scroll-driven animations
}
 
// Feature detection for Speculation Rules
if (HTMLScriptElement.supports && HTMLScriptElement.supports('speculationrules')) {
  // Inject speculation rules
}

Step 2: Use Progressive Enhancement

Structure your CSS so that baseline styles work in all browsers, and enhanced styles are applied conditionally with @supports. This ensures that users on older browsers or browsers that haven't implemented a feature yet still get a functional experience.

/* Base styles without anchor positioning */
.tooltip {
  position: fixed;
  top: 50%;
  left: 50%;
}
 
/* Enhanced styles with anchor positioning */
@supports (anchor-name: --test) {
  .trigger {
    anchor-name: --trigger;
  }
  .tooltip {
    position-anchor: --trigger;
    top: anchor(bottom);
    left: anchor(center);
  }
}

Step 3: Update Your Browser Support Policy

// .browserslistrc
> 0.5%
last 2 versions
not dead
not op_mini all

Common Pitfalls

PitfallImpactSolution
Using features before InteropBroken in some browsersCheck Interop dashboard
Not providing fallbacksBroken in older browsersUse @supports for progressive enhancement
Ignoring edge casesInconsistent behaviorTest thoroughly across browsers
Keeping old polyfillsUnnecessary bundle sizeRemove polyfills for Interop features
Assuming Baseline = zero bugsEdge cases still existTest critical paths across browsers

Testing Strategies

Effective cross-browser testing in 2025 combines automated and manual approaches. Use Playwright or Selenium to run your test suite across Chromium, Firefox, and WebKit engines. BrowserStack or Sauce Labs provide access to real devices and browser versions for manual testing. Focus manual testing on areas where browser differences are most likely: CSS layout, form inputs, media playback, and accessibility.

// Playwright cross-browser testing
import { test, expect } from '@playwright/test';
 
test('tooltip positions correctly via anchor', async ({ page }) => {
  await page.goto('/demo');
  await page.hover('.trigger');
  const tooltip = page.locator('.tooltip');
  await expect(tooltip).toBeVisible();
  const box = await tooltip.boundingBox();
  expect(box.y).toBeGreaterThan(0); // Below the trigger
});

Set up visual regression testing with tools like Percy or Chromatic to catch rendering differences that functional tests might miss. Run your test suite against multiple browser versions including beta and nightly builds to catch regressions early.

CSS Interop Improvements

CSS interoperability has improved dramatically in recent years. The :has() selector, which enables parent selection and complex relational queries, is now available in all major browsers. CSS nesting reduces the need for preprocessors. Container queries provide component-level responsive design. Subgrid enables aligned grid layouts across nested components. The @layer at-rule manages specificity without resorting to hacks.

/* CSS nesting — now interoperable */
.card {
  background: white;
  border-radius: 8px;
 
  & .title {
    font-size: 1.25rem;
    font-weight: 600;
  }
 
  &:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  }
}
 
/* Container queries — component-level responsive design */
.card-wrapper {
  container-type: inline-size;
  container-name: card;
}
 
@container card (min-width: 400px) {
  .card {
    display: flex;
    gap: 16px;
  }
}

These features eliminate common pain points that previously required browser-specific workarounds or JavaScript solutions. Test your CSS across browsers using tools like BrowserStack to catch rendering differences, but expect far fewer issues than in previous years.

JavaScript Engine Convergence

JavaScript engine behavior has converged significantly across browsers. Features like optional chaining, nullish coalescing, and top-level await work identically across all engines. The Temporal API proposal for modern date handling is being implemented consistently across browsers. Private class fields and methods follow the same specification in all engines.

// Optional chaining — consistent across all engines
const city = user?.address?.city ?? 'Unknown';
 
// Top-level await — works in all modern engines
const data = await fetch('/api/data').then(r => r.json());
 
// Private class fields — consistent behavior
class Counter {
  #count = 0;
 
  increment() {
    this.#count++;
  }
 
  get value() {
    return this.#count;
  }
}

This convergence means that developers can write modern JavaScript with confidence that it will behave the same way in Chrome, Firefox, and Safari. Use the ECMAScript compatibility table to verify that specific features are supported in your target browsers, and polyfill only the features that are not yet universal.

Best Practices

  1. Use progressive enhancement: Provide fallbacks for newer features using @supports and feature detection.
  2. Check the Interop dashboard: Track which features are ready for production at wpt.fyi/interop-2025.
  3. Feature detect: Use CSS.supports() and feature detection before using new APIs.
  4. Test across browsers: Even interoperable features may have edge cases — test critical paths in Chromium, Firefox, and WebKit.
  5. Update polyfills: Remove polyfills for features that are now interoperable to reduce bundle size.
  6. Leverage Baseline: Use the web-features package to programmatically check feature support in your build pipeline.
  7. Contribute to WPT: If you find a cross-browser inconsistency, file a bug and contribute a test case to the Web Platform Tests project.

Conclusion

The Interop project has dramatically improved cross-browser compatibility. Features like CSS Anchor Positioning, scroll-driven animations, view transitions, and speculation rules are now interoperable across all major browsers. This means developers can use these features confidently without worrying about browser-specific quirks.

Key takeaways:

  1. Interop 2025 focuses on CSS anchor positioning, scroll-driven animations, view transitions, invoker commands, and speculation rules.
  2. Exclusive accordions and :user-valid/:user-invalid are now native.
  3. Speculation Rules enable browser-optimized prefetching and prerendering.
  4. Progressive enhancement is still important for newer features — use @supports and feature detection.
  5. Check the Interop dashboard at wpt.fyi/interop-2025 to track feature readiness.
  6. Baseline provides clear guidance on which features are safe for production use.

The progress made in cross-browser interoperability demonstrates the value of collaboration between browser vendors and the web standards community. For the latest information on browser support and interoperability, consult the Interop 2025 dashboard, Baseline, and Can I Use.