Modern HTML Features (2026+) - Experimental APIs
1. HTML5.1 and HTML5.2 New Features
| Feature | Version | Description | Browser Support |
|---|---|---|---|
| Multiple <main> elements | HTML5.1 | Allow multiple <main> if only one visible (using hidden) | All modern browsers |
| Menu and menuitem DEPRECATED | HTML5.1 | Context menus (removed in HTML5.2) | Deprecated, use custom |
| details and summary | HTML5.1 | Native disclosure widget (accordion/collapse) | All modern browsers |
| dialog element | HTML5.2 | Native modal and non-modal dialogs | Chrome 37+, Firefox 98+, Safari 15.4+ |
| iframe allowpaymentrequest | HTML5.2 | Allow Payment Request API in iframe | Chrome, Edge |
| Multiple values for rel | HTML5.1 | <link rel="icon stylesheet"> space-separated | All browsers |
| Subresource Integrity (SRI) | HTML5.1 | integrity attribute for CDN security | All modern browsers |
| picture element | HTML5.1 | Art direction and responsive images | All modern browsers |
| srcset attribute | HTML5.1 | Responsive image sources | All modern browsers |
| download attribute | HTML5.1 | Force download instead of navigation | All browsers (cross-origin limited) |
Example: HTML5.1 and HTML5.2 features
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Modern HTML Features</title>
<!-- Subresource Integrity (HTML5.1) -->
<link rel="stylesheet"
href="https://cdn.example.com/styles.css"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux..."
crossorigin="anonymous">
<script src="https://cdn.example.com/script.js"
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpb..."
crossorigin="anonymous"></script>
</head>
<body>
<!-- Multiple main elements (only one visible) - HTML5.1 -->
<main id="page1">
<h1>Page 1</h1>
<p>Content for page 1</p>
</main>
<main id="page2" hidden>
<h1>Page 2</h1>
<p>Content for page 2</p>
</main>
<!-- details/summary disclosure widget - HTML5.1 -->
<details>
<summary>Click to expand</summary>
<p>Hidden content that can be toggled</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
</details>
<!-- dialog element - HTML5.2 -->
<dialog id="myDialog">
<h2>Dialog Title</h2>
<p>This is a native HTML dialog</p>
<button onclick="document.getElementById('myDialog').close()">Close</button>
</dialog>
<button onclick="document.getElementById('myDialog').showModal()">Open Dialog</button>
<!-- picture element with art direction - HTML5.1 -->
<picture>
<source media="(min-width: 1200px)" srcset="large.webp" type="image/webp">
<source media="(min-width: 768px)" srcset="medium.webp" type="image/webp">
<source srcset="small.webp" type="image/webp">
<img src="fallback.jpg" alt="Responsive image">
</picture>
<!-- Download attribute - HTML5.1 -->
<a href="/files/document.pdf" download="my-document.pdf">Download PDF</a>
<!-- iframe with allowpaymentrequest - HTML5.2 -->
<iframe src="https://payment.example.com"
allowpaymentrequest
sandbox="allow-scripts allow-same-origin">
</iframe>
</body>
</html>
HTML Evolution: HTML5.1 (Nov 2016) focused on accessibility and developer experience. HTML5.2
(Dec 2017) added dialog, removed obsolete features. HTML Living Standard (WHATWG) now guides development with
continuous updates rather than versioned releases.
2. Popover API (popover attribute) and Native Modals
| Attribute/Method | Syntax | Description | Browser Support |
|---|---|---|---|
| popover attribute NEW | <div popover>...</div> |
Declarative popover without JavaScript | Chrome 114+, Edge 114+, Safari 17+ |
| popover="auto" | <div popover="auto"> |
Light dismiss (click outside or Esc closes) | Default behavior for popover |
| popover="manual" | <div popover="manual"> |
Manual control (no light dismiss) | Requires explicit hide/toggle |
| popovertarget | <button popovertarget="id"> |
Button to toggle popover | Toggles visibility of target popover |
| popovertargetaction | popovertargetaction="show|hide|toggle" |
Specify action: show, hide, or toggle | Default is "toggle" |
| showPopover() | element.showPopover() |
JavaScript method to show popover | Programmatic control |
| hidePopover() | element.hidePopover() |
JavaScript method to hide popover | Programmatic control |
| togglePopover() | element.togglePopover() |
JavaScript method to toggle popover | Programmatic control |
| ::backdrop pseudo-element | [popover]:popover-open::backdrop |
Style the backdrop behind popover | CSS styling for overlay |
Example: Popover API usage
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Popover API Demo</title>
<style>
/* Style the popover */
[popover] {
padding: 1rem;
border: 1px solid #ccc;
border-radius: 8px;
background: white;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
/* Style the backdrop (for manual popovers) */
[popover]:popover-open::backdrop {
background: rgba(0, 0, 0, 0.5);
}
/* Animation when opening */
@starting-style {
[popover]:popover-open {
opacity: 0;
transform: translateY(-10px);
}
}
[popover]:popover-open {
opacity: 1;
transform: translateY(0);
transition: opacity 0.3s, transform 0.3s;
}
</style>
</head>
<body>
<!-- Basic auto popover (light dismiss) -->
<button popovertarget="popover1">Open Popover</button>
<div id="popover1" popover>
<h3>Auto Popover</h3>
<p>Click outside or press Esc to close.</p>
</div>
<!-- Manual popover (no light dismiss) -->
<button popovertarget="popover2" popovertargetaction="show">Show Manual Popover</button>
<button popovertarget="popover2" popovertargetaction="hide">Hide Manual Popover</button>
<div id="popover2" popover="manual">
<h3>Manual Popover</h3>
<p>Must explicitly close this popover.</p>
<button popovertarget="popover2" popovertargetaction="hide">Close</button>
</div>
<!-- Tooltip-style popover -->
<button popovertarget="tooltip1">Hover for info</button>
<div id="tooltip1" popover>
<p>This is helpful information!</p>
</div>
<!-- Programmatic control -->
<button id="customBtn">Open with JS</button>
<div id="popover3" popover>
<h3>JavaScript Controlled</h3>
<p>Opened via JavaScript method</p>
</div>
<script>
document.getElementById('customBtn').addEventListener('click', () => {
document.getElementById('popover3').showPopover();
});
// Listen to popover events
document.getElementById('popover1').addEventListener('toggle', (e) => {
console.log('Popover state:', e.newState); // 'open' or 'closed'
});
document.getElementById('popover1').addEventListener('beforetoggle', (e) => {
console.log('Before toggle:', e.oldState, '->', e.newState);
// Can prevent opening/closing here
});
</script>
</body>
</html>
| Feature | Popover API | Dialog Element | Custom Modal |
|---|---|---|---|
| Light dismiss | ✓ Built-in (auto mode) | ✓ With backdrop click handler | ✗ Manual implementation |
| Backdrop | ✓ ::backdrop pseudo-element | ✓ ::backdrop pseudo-element | ✗ Must create overlay div |
| Accessibility | Basic (no focus trap by default) | ✓ Full (focus trap, ARIA) | ✗ Requires manual ARIA |
| Top layer | ✓ Always on top | ✓ Always on top | ✗ z-index required |
| Use case | Tooltips, menus, pickers | Modals, alerts, forms | Complex custom UI |
Browser Support: Popover API is new (2023). Use feature detection:
if ('popover' in HTMLElement.prototype). Provide fallback for older browsers using polyfills or
JavaScript-based alternatives.
3. Declarative Shadow DOM (shadowrootmode)
| Feature | Syntax | Description | Browser Support |
|---|---|---|---|
| shadowrootmode NEW | <template shadowrootmode="open"> |
Declarative shadow DOM in HTML (no JavaScript needed) | Chrome 90+, Edge 91+, Safari 16.4+ |
| shadowrootmode="open" | Open shadow root | Shadow root accessible via element.shadowRoot | JavaScript can access shadow DOM |
| shadowrootmode="closed" | Closed shadow root | Shadow root not accessible from outside | Maximum encapsulation |
| shadowrootdelegatesfocus | shadowrootdelegatesfocus |
Delegate focus to first focusable element | Improves keyboard navigation |
| getHTML() NEW | element.getHTML({serializableShadowRoots: true}) |
Serialize element including shadow DOM | Chrome 125+ |
Example: Declarative Shadow DOM
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Declarative Shadow DOM</title>
</head>
<body>
<!-- Declarative shadow DOM component (no JavaScript!) -->
<my-card>
<template shadowrootmode="open">
<style>
/* Encapsulated styles - won't leak out */
:host {
display: block;
border: 1px solid #ccc;
border-radius: 8px;
padding: 1rem;
background: #f9f9f9;
}
h2 {
color: #007acc;
margin: 0 0 0.5rem 0;
}
::slotted(p) {
color: #333;
line-height: 1.6;
}
</style>
<div class="card">
<h2><slot name="title">Default Title</slot></h2>
<div class="content">
<slot>Default content</slot>
</div>
</div>
</template>
<!-- Light DOM content projected into slots -->
<span slot="title">My Custom Title</span>
<p>This is the card content.</p>
<p>Multiple paragraphs work too.</p>
</my-card>
<!-- Another instance with different content -->
<my-card>
<template shadowrootmode="open">
<style>
:host {
display: block;
border: 2px solid #28a745;
padding: 1rem;
}
h2 { color: #28a745; }
</style>
<h2><slot name="title"></slot></h2>
<slot></slot>
</template>
<span slot="title">Success Card</span>
<p>Operation completed successfully!</p>
</my-card>
<!-- Closed shadow root (maximum encapsulation) -->
<secure-component>
<template shadowrootmode="closed">
<style>
.private { background: #ff0000; }
</style>
<div class="private">This shadow DOM cannot be accessed from outside</div>
</template>
</secure-component>
<!-- With focus delegation -->
<custom-form>
<template shadowrootmode="open" shadowrootdelegatesfocus>
<style>
:host(:focus-within) {
outline: 2px solid #007acc;
outline-offset: 2px;
}
</style>
<form>
<input type="text" placeholder="Focus delegated here">
<button>Submit</button>
</form>
</template>
</custom-form>
<script>
// Feature detection and polyfill
if (!HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode')) {
console.log('Declarative Shadow DOM not supported, using polyfill');
// Polyfill: Convert declarative to imperative
document.querySelectorAll('template[shadowrootmode]').forEach(template => {
const mode = template.getAttribute('shadowrootmode');
const shadowRoot = template.parentNode.attachShadow({ mode });
shadowRoot.appendChild(template.content);
template.remove();
});
}
// Access open shadow root
const card = document.querySelector('my-card');
console.log('Shadow root:', card.shadowRoot); // accessible if "open"
// Serialize including shadow DOM (Chrome 125+)
if (card.getHTML) {
const html = card.getHTML({ serializableShadowRoots: true });
console.log('Serialized:', html);
}
</script>
</body>
</html>
Benefits of Declarative Shadow DOM:
- SSR-friendly (works without JavaScript)
- No Flash of Unstyled Content (FOUC)
- Better for performance and SEO
- Simpler than imperative attachShadow()
- Progressive enhancement ready
When to Use:
- Server-rendered Web Components
- Style encapsulation without JS
- Reusable UI patterns
- Design system components
- Email-safe components (limited)
4. Import Maps (<script type="importmap">) and ES Modules
| Feature | Syntax | Description | Browser Support |
|---|---|---|---|
| Import Maps NEW | <script type="importmap"> |
Control module resolution without bundler | Chrome 89+, Edge 89+, Safari 16.4+ |
| imports mapping | "imports": {"lib": "./lib.js"} |
Map bare module specifiers to URLs | Simplifies import statements |
| scopes mapping | "scopes": {"/admin/": {...}} |
Scope-specific module resolution | Different versions per path |
| type="module" | <script type="module" src="app.js"> |
ES6 module script | All modern browsers |
| Dynamic import() | import('./module.js').then(...) |
Lazy load modules at runtime | All modern browsers |
| import.meta | import.meta.url |
Module metadata (URL, resolve) | All modern browsers |
| Top-level await | const data = await fetch(...) |
Await at module top level | Chrome 89+, Firefox 89+, Safari 15+ |
Example: Import Maps and ES Modules
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Import Maps Demo</title>
<!-- Define import map BEFORE any module scripts -->
<script type="importmap">
{
"imports": {
"lodash": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.js",
"react": "https://esm.sh/react@18",
"react-dom": "https://esm.sh/react-dom@18",
"utils/": "./src/utils/",
"components/": "./src/components/",
"@app/config": "./config.js"
},
"scopes": {
"/admin/": {
"utils/": "./admin/utils/"
}
}
}
</script>
</head>
<body>
<div id="app"></div>
<!-- Module script using import map -->
<script type="module">
// Use mapped imports (no ./ or full URL needed)
import { debounce } from 'lodash';
import React from 'react';
import { render } from 'react-dom';
import config from '@app/config';
// Use path mappings
import { formatDate } from 'utils/formatters.js';
import Button from 'components/Button.js';
console.log('Config:', config);
console.log('Debounce:', debounce);
// Dynamic import (code splitting)
const loadModule = async () => {
const module = await import('utils/heavy-module.js');
module.initialize();
};
document.querySelector('#load-btn')?.addEventListener('click', loadModule);
</script>
<!-- Top-level await example -->
<script type="module">
// Fetch data at module top level
const response = await fetch('/api/config');
const config = await response.json();
console.log('Config loaded:', config);
// Module waits for this to complete before executing dependent modules
</script>
<!-- import.meta usage -->
<script type="module">
console.log('Current module URL:', import.meta.url);
// Resolve relative URLs
const imageUrl = new URL('./images/logo.png', import.meta.url);
console.log('Image URL:', imageUrl.href);
// Feature detection
if (import.meta.resolve) {
const resolved = import.meta.resolve('lodash');
console.log('Lodash resolves to:', resolved);
}
</script>
<!-- Module with exports -->
<script type="module">
// Named exports
export const API_URL = 'https://api.example.com';
export function fetchData() { /* ... */ }
// Default export
export default class App { /* ... */ }
</script>
<!-- Import from inline module -->
<script type="module">
// Can't directly import from inline modules in same document
// Use external files or dynamic import
const module = await import('./app.js');
module.default.init();
</script>
<!-- Polyfill for older browsers -->
<script>
if (!HTMLScriptElement.supports || !HTMLScriptElement.supports('importmap')) {
// Load es-module-shims polyfill
const script = document.createElement('script');
script.src = 'https://ga.jspm.io/npm:es-module-shims@1.8.0/dist/es-module-shims.js';
script.async = true;
document.head.appendChild(script);
}
</script>
</body>
</html>
| Import Type | Before Import Maps | With Import Maps | Benefit |
|---|---|---|---|
| CDN Module | import {...} from 'https://cdn.../lib.js' |
import {...} from 'lib' |
Cleaner, centralized URL management |
| Local Module | import {...} from './src/utils/helper.js' |
import {...} from 'utils/helper.js' |
Path aliasing, easier refactoring |
| Version Control | Update all import statements | Update import map once | Centralized dependency management |
| Environment | Build-time replacement | Runtime mapping | No build step needed |
5. Web Streams API and Readable Streams
| API | Description | Use Case | Browser Support |
|---|---|---|---|
| ReadableStream | Stream of data chunks that can be read progressively | Large file processing, infinite scrolls | All modern browsers |
| WritableStream | Destination for streaming data | File writes, network uploads | All modern browsers |
| TransformStream | Transform data while streaming | Compression, encryption, parsing | All modern browsers |
| Response.body (Fetch) | ReadableStream from fetch response | Progressive rendering, download progress | All modern browsers |
| async iteration | for await...of loop over streams | Simplified stream consumption | All modern browsers |
| TextDecoderStream | Decode byte stream to text | UTF-8 decoding, text processing | Chrome 71+, Firefox 105+, Safari 14.1+ |
| CompressionStream | Compress data stream (gzip, deflate) | Client-side compression | Chrome 80+, Firefox 113+, Safari 16.4+ |
Example: Web Streams API usage
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Web Streams Demo</title>
</head>
<body>
<div id="output"></div>
<div id="progress"></div>
<script>
// Example 1: Fetch with streaming response
async function fetchWithProgress(url) {
const response = await fetch(url);
const reader = response.body.getReader();
const contentLength = +response.headers.get('Content-Length');
let receivedLength = 0;
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
receivedLength += value.length;
// Update progress
const percent = (receivedLength / contentLength * 100).toFixed(2);
document.getElementById('progress').textContent = `${percent}% loaded`;
}
// Concatenate chunks into single array
const chunksAll = new Uint8Array(receivedLength);
let position = 0;
for (let chunk of chunks) {
chunksAll.set(chunk, position);
position += chunk.length;
}
return chunksAll;
}
// Example 2: Async iteration over stream
async function streamText(url) {
const response = await fetch(url);
const reader = response.body
.pipeThrough(new TextDecoderStream())
.getReader();
for await (const chunk of readStream(reader)) {
document.getElementById('output').textContent += chunk;
}
}
async function* readStream(reader) {
while (true) {
const { done, value } = await reader.read();
if (done) return;
yield value;
}
}
// Example 3: Transform stream (uppercase)
async function transformStream(text) {
const stream = new ReadableStream({
start(controller) {
controller.enqueue(text);
controller.close();
}
});
const transformedStream = stream.pipeThrough(
new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk.toUpperCase());
}
})
);
const reader = transformedStream.getReader();
const { value } = await reader.read();
return value;
}
// Example 4: Create custom ReadableStream
const customStream = new ReadableStream({
start(controller) {
let i = 0;
const interval = setInterval(() => {
if (i < 10) {
controller.enqueue(`Chunk ${i}\n`);
i++;
} else {
controller.close();
clearInterval(interval);
}
}, 100);
}
});
// Example 5: Compression stream
async function compressData(text) {
const blob = new Blob([text]);
const stream = blob.stream();
const compressedStream = stream.pipeThrough(
new CompressionStream('gzip')
);
const compressedBlob = await new Response(compressedStream).blob();
console.log('Original:', blob.size, 'Compressed:', compressedBlob.size);
return compressedBlob;
}
// Example 6: Pipe streams
async function pipeExample() {
const response = await fetch('/data.txt');
// Chain multiple transforms
const processedStream = response.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(new TransformStream({
transform(chunk, controller) {
// Process each chunk
const processed = chunk.trim().toUpperCase();
controller.enqueue(processed);
}
}));
// Write to destination
const writableStream = new WritableStream({
write(chunk) {
console.log('Processed chunk:', chunk);
document.getElementById('output').textContent += chunk;
}
});
await processedStream.pipeTo(writableStream);
}
// Example 7: Server-Sent Events with streams
async function streamSSE(url) {
const response = await fetch(url);
const reader = response.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(new TransformStream({
transform(chunk, controller) {
// Parse SSE format
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
controller.enqueue(JSON.parse(data));
}
}
}
}))
.getReader();
for await (const event of readStream(reader)) {
console.log('SSE event:', event);
}
}
</script>
</body>
</html>
Stream Benefits: Memory efficient for large files (process chunk-by-chunk), enables progress
indicators, supports backpressure (slow consumer signals producer), composable (chain transformations), works
with Service Workers for offline-first apps.
6. Navigation API and SPA Routing
| API Feature | Method/Event | Description | Browser Support |
|---|---|---|---|
| Navigation API NEW | navigation.navigate(url) |
Modern replacement for History API | Chrome 102+, Edge 102+ |
| navigation.navigate() | navigation.navigate('/page', {state}) |
Navigate to URL with state | Cleaner than pushState |
| navigation.back() | navigation.back() |
Navigate backward in history | Equivalent to history.back() |
| navigation.forward() | navigation.forward() |
Navigate forward in history | Equivalent to history.forward() |
| navigation.reload() | navigation.reload() |
Reload current entry | Programmatic reload |
| navigate event | navigation.addEventListener('navigate') |
Intercept all navigations (links, forms, browser UI) | Single event for all navigation types |
| navigatesuccess event | navigation.addEventListener('navigatesuccess') |
Navigation completed successfully | Update UI, analytics |
| navigateerror event | navigation.addEventListener('navigateerror') |
Navigation failed | Error handling |
| navigation.currentEntry | navigation.currentEntry.url |
Current navigation entry | Access URL, state, key, index |
| View Transitions NEW | document.startViewTransition() |
Animated transitions between pages | Chrome 111+, Edge 111+ |
Example: Navigation API for SPA routing
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Navigation API Demo</title>
<style>
/* View Transitions API */
@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 {
to { opacity: 0; }
}
@keyframes fade-in {
from { opacity: 0; }
}
</style>
</head>
<body>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
<main id="content"></main>
<script>
// Check for Navigation API support
if ('navigation' in window) {
console.log('Navigation API supported!');
// Intercept all navigations
navigation.addEventListener('navigate', (event) => {
// Only handle same-origin navigations
if (!event.canIntercept || event.hashChange || event.downloadRequest) {
return;
}
const url = new URL(event.destination.url);
// Intercept and handle as SPA navigation
event.intercept({
async handler() {
// Fetch new content
const response = await fetch(url.pathname);
const html = await response.text();
// Use View Transitions API if available
if (document.startViewTransition) {
await document.startViewTransition(() => {
document.getElementById('content').innerHTML = html;
}).finished;
} else {
document.getElementById('content').innerHTML = html;
}
// Update page title
document.title = `Page - ${url.pathname}`;
},
// Optional: scroll to top, focus management
scroll: 'after-transition',
focusReset: 'after-transition'
});
});
// Listen to successful navigation
navigation.addEventListener('navigatesuccess', (event) => {
console.log('Navigation succeeded:', navigation.currentEntry.url);
// Analytics
gtag?.('event', 'page_view', {
page_path: navigation.currentEntry.url
});
});
// Handle navigation errors
navigation.addEventListener('navigateerror', (event) => {
console.error('Navigation failed:', event.error);
document.getElementById('content').innerHTML = '<h1>Error loading page</h1>';
});
// Programmatic navigation
function navigateTo(path, state = {}) {
navigation.navigate(path, { state, history: 'push' });
}
// Navigation with state
document.querySelector('#special-btn')?.addEventListener('click', () => {
navigation.navigate('/dashboard', {
state: { userId: 123, fromButton: true }
});
});
// Access current navigation entry
console.log('Current URL:', navigation.currentEntry.url);
console.log('Current state:', navigation.currentEntry.getState());
console.log('Current key:', navigation.currentEntry.key);
console.log('Current index:', navigation.currentEntry.index);
// Traversal (back/forward)
function goBack() {
if (navigation.canGoBack) {
navigation.back();
}
}
function goForward() {
if (navigation.canGoForward) {
navigation.forward();
}
}
// Get all entries
const entries = navigation.entries();
console.log('Total entries:', entries.length);
// Navigate to specific entry
navigation.traverseTo(entries[0].key);
} else {
console.log('Navigation API not supported, using History API fallback');
// Fallback to traditional History API
window.addEventListener('popstate', (event) => {
loadPage(location.pathname, event.state);
});
document.addEventListener('click', (e) => {
if (e.target.tagName === 'A' && e.target.host === location.host) {
e.preventDefault();
const path = e.target.pathname;
history.pushState({}, '', path);
loadPage(path);
}
});
async function loadPage(path, state = {}) {
const response = await fetch(path);
const html = await response.text();
document.getElementById('content').innerHTML = html;
}
}
// View Transitions API (standalone)
async function updateContent(newContent) {
if (!document.startViewTransition) {
// Fallback: instant update
document.getElementById('content').innerHTML = newContent;
return;
}
// Animated transition
const transition = document.startViewTransition(() => {
document.getElementById('content').innerHTML = newContent;
});
// Wait for transition to complete
await transition.finished;
console.log('Transition complete');
}
</script>
</body>
</html>
| Feature | History API (Old) | Navigation API (New) | Improvement |
|---|---|---|---|
| Navigation events | popstate only (browser back/forward) | navigate (all navigations) | Single event for links, forms, browser UI |
| State management | pushState(), replaceState() | navigation.navigate() | Cleaner API, promises |
| Async handling | Manual promise management | event.intercept({ async handler }) | Built-in async support |
| Scroll/focus | Manual implementation | scroll, focusReset options | Automatic scroll restoration |
| Navigation entries | Limited access | navigation.entries(), currentEntry | Full history access, metadata |
| Error handling | Manual try/catch | navigateerror event | Centralized error handling |
Browser Support Warning: Many features in this section are experimental or newly standardized.
Always check caniuse.com for current support. Use feature detection:
if ('navigation' in window),
if (HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode')). Provide polyfills or graceful
degradation for production apps.