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 Baseline 2025: All Major Features Widely Available

CSS nesting, container queries, view transitions, and more now in all browsers.

Web PlatformBaselineCSSFrontend

By MinhVo

Introduction

The web platform reached a major milestone in 2025. Features that developers waited years for are now Baseline Widely Available—meaning they work in all major browsers (Chrome, Firefox, Safari, Edge) and have been available for at least 30 months. This guide covers the most impactful features that are now safe to use in production without polyfills or workarounds.

Web platform evolution

What is Baseline?

Baseline is a WebDX Community Group initiative that tracks browser support for web platform features. Each feature is classified as:

  • Baseline Limited: Supported in all major browsers but recently (less than 30 months)
  • Baseline Widely Available: Supported in all major browsers for 30+ months
  • Not yet Baseline: Missing from at least one major browser

This gives developers clear guidance on when it's safe to adopt new features.

CSS Features Now Baseline Widely Available

CSS Nesting

Native CSS nesting eliminates the need for preprocessors like Sass for basic nesting.

/* Before: required Sass or Less */
.card {
  & .title {
    font-size: 1.5rem;
  }
  & .content {
    line-height: 1.6;
  }
  &:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  }
}
 
/* Also works with the & selector for specificity */
.card {
  & > .title {
    font-weight: bold;
  }
 
  /* Nested media queries */
  @media (min-width: 768px) {
    display: grid;
    grid-template-columns: 1fr 2fr;
  }
 
  /* Nested container queries */
  @container (min-width: 400px) {
    flex-direction: row;
  }
}

Container Queries

Components respond to their container's size, not the viewport.

.card-wrapper {
  container-type: inline-size;
  container-name: card;
}
 
@container card (min-width: 400px) {
  .card {
    display: grid;
    grid-template-columns: 200px 1fr;
  }
}
 
@container card (max-width: 399px) {
  .card {
    display: flex;
    flex-direction: column;
  }
}

Cascade Layers

Explicit control over style specificity without !important.

@layer reset, base, components, utilities;
 
@layer reset {
  * { margin: 0; padding: 0; box-sizing: border-box; }
}
 
@layer components {
  .button { padding: 8px 16px; border-radius: 4px; }
}
 
@layer utilities {
  .hidden { display: none; }
}

:has() Selector

Parent selection based on child content—the "parent selector" developers requested for decades.

/* Style a card differently when it contains an image */
.card:has(img) {
  grid-template-columns: 200px 1fr;
}
 
/* Style a form group when its input is invalid */
.form-group:has(:invalid) {
  border-color: red;
}
 
/* Style a section when it's empty */
.section:has(:empty) {
  display: none;
}
 
/* Complex selectors */
article:has(h2, h3) .title {
  font-size: 2rem;
}

Subgrid

Nested grid items align with parent grid tracks.

.card-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
}
 
.card {
  display: grid;
  grid-row: span 3;
  grid-template-rows: subgrid;
}

CSS features

JavaScript APIs Now Baseline Widely Available

Structured Clone

Deep clone any JavaScript value, including complex types that JSON.parse(JSON.stringify()) cannot handle.

// Clone objects with circular references, Date, Map, Set, etc.
const original = {
  date: new Date(),
  map: new Map([['key', 'value']]),
  set: new Set([1, 2, 3]),
  nested: { deep: { value: 42 } },
};
 
const clone = structuredClone(original);
clone.nested.deep.value = 100;
console.log(original.nested.deep.value); // 42 — not affected

Array.at() and String.at()

Access elements from the end of arrays and strings.

const arr = [1, 2, 3, 4, 5];
arr.at(-1); // 5
arr.at(-2); // 4
 
const str = 'hello';
str.at(-1); // 'o'
str.at(-2); // 'l'

Object.hasOwn()

Clean replacement for Object.prototype.hasOwnProperty.call().

const obj = { key: 'value' };
 
// Before
Object.prototype.hasOwnProperty.call(obj, 'key');
 
// After
Object.hasOwn(obj, 'key');

Array.findLast() and Array.findLastIndex()

Search from the end of an array.

const numbers = [1, 2, 3, 4, 5, 6, 7, 8];
 
numbers.findLast(n => n % 2 === 0); // 8
numbers.findLastIndex(n => n % 2 === 0); // 7

RegExp v Flag

Set notation for Unicode property escapes.

// Match characters that are letters but not ASCII
const nonAscii = /\p{Letter}/v;
 
// Set operations: union, intersection, difference
const emoji = /[\p{Emoji}--\p{Emoji_Component}]/v;
 
// Named character classes
const latin = /[\p{Script=Latin}]/v;

APIs Now Baseline Widely Available

View Transitions API

// Same-document view transitions
document.startViewTransition(() => {
  updateDOM();
});
 
// Named view transitions for specific elements
.hero {
  view-transition-name: hero;
}
 
::view-transition-old(hero) {
  animation: slide-out 0.3s ease-out;
}
 
::view-transition-new(hero) {
  animation: slide-in 0.3s ease-in;
}

Web Share API

async function shareContent() {
  if (navigator.share) {
    await navigator.share({
      title: 'Check this out',
      text: 'Amazing article about web development',
      url: window.location.href,
    });
  }
}

Popover API

<!-- Declarative popover -->
<button popovertarget="my-popover">Open</button>
<div id="my-popover" popover>
  <p>Popover content</p>
</div>
/* Style popovers */
[popover] {
  padding: 16px;
  border: 1px solid #ccc;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
 
/* Animation on open/close */
[popover]:popover-open {
  animation: slide-in 0.2s ease-out;
}
 
[popover]:not(:popover-open) {
  animation: slide-out 0.2s ease-in;
}

Browser compatibility

Using Baseline in Your Project

Web-features Package

npm install web-features
import { features } from 'web-features';
 
// Check if a feature is Baseline Widely Available
const feature = features['css-nesting'];
console.log(feature.status.baseline); // "high" = Widely Available

ESLint Plugin

// .eslintrc.json
{
  "plugins": ["@anthropic/baseline"],
  "rules": {
    "@anthropic/baseline/no-non-baseline": "warn"
  }
}

Best Practices

  1. Use Baseline as your adoption signal: If a feature is Baseline Widely Available, it's safe to use
  2. Progressive enhancement for newer features: Use @supports or feature detection for Limited features
  3. Remove polyfills: Many polyfills are no longer needed for Baseline features
  4. Update your browser support policy: Baseline Widely Available means 30+ months of support
  5. Check web-features package: Programmatic access to Baseline status

Common Pitfalls

PitfallImpactSolution
Using features before BaselineBroken in older browsersCheck Baseline status first
Keeping unnecessary polyfillsLarger bundle sizeRemove polyfills for Baseline features
Assuming all features are BaselineSome newer APIs aren't yetUse web-features to check
Ignoring Safari supportiOS users affectedVerify Safari support for each feature

Impact on Development Practices

The convergence of browser support in twenty twenty five changes how developers write CSS and JavaScript. The need for CSS prefixes has largely disappeared, with autoprefixer processing only a tiny fraction of properties compared to five years ago. Polyfills are needed only for niche features, reducing JavaScript bundle sizes. The gap between what is possible on the web and what is possible in native applications continues to narrow. Developers can confidently use modern APIs knowing that ninety five percent or more of their users have access to them. This reduces the cognitive load of cross-browser compatibility and lets developers focus on building features rather than working around browser differences.

CSS Container Queries in Practice

CSS container queries have matured into a widely available feature that changes how developers think about responsive design. Instead of querying the viewport size, container queries let you query the size of a parent container. This makes components truly reusable because they adapt to their container regardless of where they are placed on the page. Implement card components that change their layout from horizontal to vertical based on container width. Create sidebar widgets that collapse their content when the sidebar is narrow. Use container query units like cqw and cqh for sizing elements relative to their container. These patterns replace complex media query combinations and make component libraries more flexible.

View Transitions Adoption

The View Transitions API has achieved baseline status, making it safe to use in production across all major browsers. Implement page transitions in single-page applications by wrapping state updates in document.startViewTransition. Style transitions using CSS to create custom animations between page states. Use named view transition elements to animate specific elements independently during transitions. The API works with both same-document transitions in SPAs and cross-document transitions in multi-page applications. Progressive enhancement ensures that browsers without View Transitions support still display content correctly, just without the animated transitions.

Declarative Shadow DOM

Declarative Shadow DOM enables server-rendered web components without JavaScript. Include a template element with the shadowrootmode attribute inside a custom element, and the browser creates the shadow root during HTML parsing. This eliminates the flash of unstyled content that occurs when web components wait for JavaScript to hydrate. Use Declarative Shadow DOM for components that need to render immediately, like headers, navigation bars, and hero sections. Combine it with client-side hydration for interactive components that need JavaScript after the initial render. This pattern bridges the gap between server rendering and client-side interactivity.

Popover API

The Popover API provides a native way to create popover elements that float above other content. Use the popover attribute to mark an element as a popover, and the popovertarget attribute on a button to toggle it. The API handles positioning, focus management, light dismiss when clicking outside, and accessibility automatically. No JavaScript is required for basic popover behavior, though you can use the beforetoggle event for custom logic. Popovers are rendered in the top layer, above all other content regardless of z-index. Use the Popover API for tooltips, menus, dialogs, and other floating content that needs to appear above the main page content.

Exclusive Accordion

The exclusive accordion pattern uses the name attribute on details elements to create accordion behavior where only one detail can be open at a time. When you open a details element that shares a name with other details elements, the others automatically close. This native behavior eliminates the need for JavaScript accordion implementations. Use exclusive accordions for FAQ sections, settings panels, and any interface where users should focus on one section at a time. Style the accordion using the ::details-content pseudo-element for smooth open and close animations. This feature is part of the broader trend of adding interactive UI patterns to HTML that previously required JavaScript.

HTTP/3 and QUIC

HTTP/3 built on the QUIC transport protocol is now widely available across all major browsers and CDN providers. HTTP/3 eliminates head-of-line blocking at the transport layer, improving performance for connections that experience packet loss. The protocol includes built-in encryption, reducing the connection establishment overhead compared to HTTP/2 over TLS. Connection migration allows HTTP/3 connections to survive network changes like switching from Wi-Fi to cellular. These protocol improvements result in faster page loads, especially on mobile networks with variable connection quality. Configure your server and CDN to serve content over HTTP/3 to take advantage of these performance improvements.

JavaScript APIs Now Baseline Widely Available

structuredClone() for Deep Copying

The structuredClone() API provides native deep cloning without the JSON.parse(JSON.stringify()) hack:

// Before: hacky deep copy
const copy = JSON.parse(JSON.stringify(original));
 
// Problems with JSON approach:
// - Loses Date objects (becomes strings)
// - Loses undefined values
// - Loses Map, Set, RegExp, Error
// - Throws on circular references
// - Loses functions
 
// After: native deep copy
const copy = structuredClone(original);
 
// Works with complex types
const data = {
    date: new Date(),
    map: new Map([['key', 'value']]),
    set: new Set([1, 2, 3]),
    regex: /pattern/gi,
    nested: { deep: { value: 42 } }
};
 
const cloned = structuredClone(data);
console.log(cloned.date instanceof Date);  // true
console.log(cloned.map instanceof Map);    // true
console.log(cloned.set instanceof Set);    // true
 
// Handles circular references
const circular = { self: null };
circular.self = circular;
const clonedCircular = structuredClone(circular);  // Works!

Array.at() and String.at() for Negative Indexing

const colors = ['red', 'green', 'blue', 'yellow', 'purple'];
 
// Before: verbose negative indexing
const last = colors[colors.length - 1];
const secondToLast = colors[colors.length - 2];
 
// After: clean negative indexing
const last = colors.at(-1);      // 'purple'
const secondToLast = colors.at(-2);  // 'yellow'
const first = colors.at(0);      // 'red'
 
// Works with strings too
const str = 'Hello, World!';
console.log(str.at(-1));   // '!'
console.log(str.at(-5));   // 'r'

Object.groupBy() and Map.groupBy()

const inventory = [
    { name: 'apple', type: 'fruit', quantity: 5 },
    { name: 'banana', type: 'fruit', quantity: 3 },
    { name: 'carrot', type: 'vegetable', quantity: 8 },
    { name: 'broccoli', type: 'vegetable', quantity: 2 },
    { name: 'chicken', type: 'meat', quantity: 4 },
];
 
// Group by type
const grouped = Object.groupBy(inventory, ({ type }) => type);
// {
//   fruit: [{ name: 'apple', ... }, { name: 'banana', ... }],
//   vegetable: [{ name: 'carrot', ... }, { name: 'broccoli', ... }],
//   meat: [{ name: 'chicken', ... }]
// }
 
// Group by quantity threshold
const byStock = Object.groupBy(inventory, ({ quantity }) =>
    quantity > 5 ? 'high' : 'low'
);
// { high: [{ name: 'carrot', ... }], low: [...] }
 
// Map.groupBy preserves non-string keys
const items = [1, 2, 3, 4, 5, 6];
const byParity = Map.groupBy(items, n => n % 2 === 0 ? 'even' : 'odd');

Promise.withResolvers()

// Before: verbose promise construction
let resolve, reject;
const promise = new Promise((res, rej) => {
    resolve = res;
    reject = rej;
});
 
// After: clean one-liner
const { promise, resolve, reject } = Promise.withResolvers();
 
// Useful for event-driven patterns
const { promise: clickPromise, resolve: clickResolve } = Promise.withResolvers();
button.addEventListener('click', clickResolve, { once: true });
await clickPromise;
 
// Useful for message-based communication
const { promise: messagePromise, resolve: messageResolve } = Promise.withResolvers();
window.addEventListener('message', (event) => {
    if (event.data.type === 'response') messageResolve(event.data);
});

The Temporal API (Stage 3, Coming Soon)

While not yet Baseline, the Temporal API is the most anticipated JavaScript feature:

// Current Date API problems
const date = new Date('2025-01-15');  // Parsed as UTC midnight
const local = new Date(2025, 0, 15);  // Month is 0-indexed
 
// Temporal solves these problems
const plainDate = Temporal.PlainDate.from('2025-01-15');
const zonedDateTime = Temporal.ZonedDateTime.from('2025-01-15[America/New_York]');
const duration = Temporal.Duration.from({ hours: 2, minutes: 30 });
 
// Duration arithmetic
const meeting = Temporal.ZonedDateTime.from('2025-01-15T10:00:00[America/New_York]');
const endMeeting = meeting.add({ hours: 1, minutes: 30 });

CSS :has() Selector in Depth

The :has() selector is often called the "parent selector" but it's much more powerful:

/* Style a card differently when it contains an image */
.card:has(img) {
    grid-template-columns: 200px 1fr;
}
 
/* Style a form group when its input is invalid */
.form-group:has(input:invalid) {
    border-color: red;
}
 
/* Style a section when it has a focused element */
section:has(:focus) {
    outline: 2px solid blue;
}
 
/* Complex: style a list item that contains both an image and a description */
li:has(img):has(.description) {
    display: grid;
    grid-template-columns: auto 1fr;
}
 
/* Negative: style cards WITHOUT a header */
.card:not(:has(h2, h3)) {
    padding-top: 2rem;
}
 
/* Conditional: style siblings based on content */
.article:has(> .featured) + .article {
    margin-top: 3rem;
}
// Use :has() with JavaScript for dynamic checks
const hasImages = document.querySelectorAll('.card:has(img)');
console.log(`Found ${hasImages.length} cards with images`);

CSS Cascade Layers

Cascade layers give you explicit control over CSS specificity:

/* Define layer order (earlier layers have lower priority) */
@layer reset, base, components, utilities;
 
@layer reset {
    * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
    }
}
 
@layer base {
    body {
        font-family: system-ui;
        line-height: 1.6;
    }
    a {
        color: blue;
    }
}
 
@layer components {
    .button {
        padding: 0.5rem 1rem;
        border-radius: 0.25rem;
    }
}
 
@layer utilities {
    .hidden {
        display: none !important;  /* No longer needs !important */
    }
}
 
/* Utilities always win over components, regardless of specificity */
/* because 'utilities' layer comes after 'components' layer */

Migration Guide: Removing Polyfills

With these features now Baseline, you can remove polyfills:

// Remove these polyfills:
// - css-vars-ponyfill (CSS custom properties)
// - focus-visible polyfill (:focus-visible)
// - intersection-observer polyfill
// - resize-observer polyfill
// - smooth-scroll polyfill (scroll-behavior: smooth)
// - array-flat polyfill (Array.flat/flatMap)
// - object-fromentries polyfill
 
// package.json: remove unnecessary dependencies
{
    "dependencies": {
        // "focus-visible": "^5.0.0",  ← REMOVE
        // "intersection-observer": "^0.12.0",  ← REMOVE
        // "smoothscroll-polyfill": "^0.4.4",  ← REMOVE
    }
}
// Update your polyfill loading to feature-detect
if (!('groupBy' in Object)) {
    // Only load polyfill if needed (very unlikely in 2025)
    import('polyfill-object-groupby');
}

New Baseline Features in 2025

Several features reached Baseline Widely Available status in 2025 that deserve special attention for production adoption.

View Transitions API

The View Transitions API enables smooth, native page transitions without JavaScript animation libraries:

@view-transition {
  navigation: auto;
}
 
::view-transition-old(root) {
  animation: fade-out 0.3s ease-out;
}
 
::view-transition-new(root) {
  animation: fade-in 0.3s ease-in;
}
 
@keyframes fade-out {
  from { opacity: 1; transform: scale(1); }
  to { opacity: 0; transform: scale(0.95); }
}
 
@keyframes fade-in {
  from { opacity: 0; transform: scale(1.05); }
  to { opacity: 1; transform: scale(1); }
}

For cross-document transitions in multi-page applications, the API automatically captures snapshots of the old and new pages and animates between them. This eliminates the need for complex JavaScript transition libraries like GSAP or Framer Motion for basic page transitions.

Popover API

The Popover API provides native, accessible popovers without JavaScript:

<!-- Trigger button -->
<button popovertarget="my-popover">Open Menu</button>
 
<!-- Popover content -->
<div id="my-popover" popover>
  <nav>
    <a href="/profile">Profile</a>
    <a href="/settings">Settings</a>
    <a href="/logout">Logout</a>
  </nav>
</div>

The browser handles positioning, focus management, light-dismiss (clicking outside closes it), and keyboard navigation automatically. This replaces dozens of lines of custom JavaScript for dropdown menus, tooltips, and modals.

dialog Element Improvements

The native <dialog> element with showModal() is now Baseline and handles focus trapping, backdrop styling, and Escape key dismissal:

dialog::backdrop {
  background: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(4px);
}
 
dialog {
  border: none;
  border-radius: 0.5rem;
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}

JavaScript APIs Now Baseline

Beyond CSS, several JavaScript APIs reached Baseline status in 2024-2025:

Structured Clone and BroadcastChannel

The structuredClone() function enables deep copying of complex JavaScript objects including Date, RegExp, Map, Set, ArrayBuffer, and Blob. Previously, developers relied on JSON.parse(JSON.stringify(obj)) which loses type information for dates, regexes, and binary data. The BroadcastChannel API enables cross-tab communication without localStorage hacks:

// Deep cloning with type preservation
const original = { date: new Date(), pattern: /test/gi, data: new ArrayBuffer(8) };
const clone = structuredClone(original);
 
// Cross-tab communication
const channel = new BroadcastChannel('app-state');
channel.postMessage({ type: 'theme-changed', theme: 'dark' });
channel.onmessage = (event) => console.log(event.data);

URL Pattern API

The URLPattern API provides native URL matching without regex:

const pattern = new URLPattern({ pathname: '/api/users/:id' });
const match = pattern.exec('https://example.com/api/users/42');
console.log(match.pathname.groups.id); // "42"

CSS Custom Highlight API

The Highlight API enables programmatic text highlighting without DOM manipulation:

const highlight = new Highlight(
  ...document.querySelectorAll('.search-match')
);
CSS.highlights.set('search-results', highlight);
::highlight(search-results) {
  background-color: yellow;
  color: black;
}

Migration Strategies for Existing Projects

Adopting Baseline features in existing projects requires a systematic approach:

Step 1: Audit current polyfill usage. Run npx polyfill-library-cli or check your bundle for known polyfills. Map each polyfill to its Baseline status.

Step 2: Update browserslist config. Ensure your browserslist in package.json targets browsers that support Baseline features:

{
  "browserslist": [
    "defaults and supports es6-module"
  ]
}

Step 3: Remove unnecessary polyfills. Delete polyfills for features now in Baseline. If using core-js, update to the latest version which excludes Baseline features from default imports.

Step 4: Replace preprocessor features. If using Sass for nesting, migrate to native CSS nesting gradually. Start with new components and migrate existing ones during refactors.

Step 5: Test across browsers. Even though features are Baseline, test in Safari, Chrome, Firefox, and Edge to catch edge cases in specific versions.

Performance Impact of Adopting Baseline Features

Adopting Baseline features has measurable performance benefits. Removing polyfills reduces bundle size and parse time:

Polyfill RemovedBundle SavingsParse Time Savings
Intersection Observer~3KB gzipped~2ms
Resize Observer~2KB gzipped~1ms
Smooth Scroll~1KB gzipped~0.5ms
Focus Visible~1KB gzipped~0.5ms
Array.flat~0.5KB gzipped~0.3ms
Total~7.5KB~4.3ms

On a 3G connection, removing 7.5KB of polyfills saves approximately 150ms of download time. Combined with the 4.3ms parse time savings, this contributes directly to improved Core Web Vitals scores.

Conclusion

The web platform in 2025 is more capable than ever. CSS nesting, container queries, cascade layers, :has(), subgrid, and dozens of JavaScript APIs are now Baseline Widely Available. This means you can use them confidently in production without polyfills or fallbacks.

Key takeaways:

  1. CSS nesting eliminates the need for Sass for basic nesting
  2. Container queries make components responsive to their container
  3. Cascade layers give explicit control over specificity
  4. :has() is the parent selector developers waited decades for
  5. Baseline provides clear signals for when to adopt new features