Search IOCombats

Search challenges, guides, questions and articles

Web Platform Baseline 2026: Three APIs You Can Ship Now, Explained From First Principles
Baseline 2026Navigation APICSS Anchor PositioningTrusted TypesWeb PlatformBrowser APIs

Web Platform Baseline 2026: Three APIs You Can Ship Now, Explained From First Principles

By Ghazi Khan | Aug 23, 2026 - 9 min read

Every year a handful of browser APIs quietly cross the line from "cool demo, three years out" to "safe to ship to production." In 2026, three of them did: the Navigation API, CSS Anchor Positioning, and the Trusted Types API. All three are now Baseline, meaning Chrome, Edge, Firefox, and Safari agree on how they behave, not just that they exist.

Most write-ups about this stop at "it shipped, here is a link." That is not enough to actually use these APIs correctly, and it is not enough to answer a follow-up question about them in an interview. This post explains what each API actually does internally, why it exists, and how to use it, with code you can run today.

What "Baseline" actually measures

Before the specifics, it is worth being precise about what Baseline means, because the term gets used loosely. Baseline is a status assigned by the web-features project (backed by the browser vendors and the W3C WebDX Community Group) to a feature once it has shipped, without a flag, in the current stable release of Chrome, Edge, Firefox, and Safari at the same time. That earns it "Newly available."

"Newly available" is not the same as "safe for every user." A feature only becomes "Widely available" after roughly 30 months of Newly available status, once older browser versions have mostly cycled out of the population. The three APIs in this post are all Newly available as of 2026, which means you should ship them with a feature check or a graceful fallback, not assume every visitor's browser supports them unconditionally.

Diagram
100%
flowchart LR A["Ships behind a flag<br/>in one browser"] --> B["Newly available<br/>(unflagged in Chrome/Edge,<br/>Firefox, and Safari<br/>at the same time)"] B -->|"~30 months"| C["Widely available<br/>(safe with no fallback)"] B -.->|"2026: you are here"| D["Navigation API<br/>CSS Anchor Positioning<br/>Trusted Types"]
visualized byIOCombats
APIChrome/EdgeFirefoxSafariBaseline since
Navigation APISupported147+26.2+January 2026
CSS Anchor Positioning125+132+18.2+ (full flip behavior needs 18.4+)2026
Trusted Types API83+148+26+February 2026

The Navigation API: fixing what the History API never modeled

To understand why the Navigation API matters, you have to understand what was actually broken. The old history.pushState() / popstate pattern that every SPA router (React Router, Vue Router, and the rest) has relied on for a decade has a structural gap: pushState changes the URL, but nothing tells you why a navigation happened, and popstate only fires for back/forward, not for link clicks or form submissions. Routers had to intercept clicks on <a> tags manually, call preventDefault(), and reconstruct intent from scratch. There was also no way to inspect the browser's session history stack directly. It was write-only.

The Navigation API replaces this with a single event-driven model built around a navigate event on window.navigation. Every navigation, whether it is a link click, a form submission, history.pushState, or the back button, fires this one event before it takes effect, and you can intercept it:

navigation.addEventListener('navigate', (event) => {
  // Ignore cross-origin navigations and downloads; you don't own those
  if (!event.canIntercept || event.hashChange || event.downloadRequest) {
    return;
  }

  const url = new URL(event.destination.url);

  event.intercept({
    async handler() {
      const view = await matchRoute(url.pathname);
      renderView(view);
    },
  });
});
Diagram
100%
sequenceDiagram participant U as User participant B as Browser participant N as Navigation API participant R as App Router U->>B: Clicks link / submits form B->>N: Fires "navigate" event N->>R: Dispatches event to listener alt event.canIntercept is true R->>N: event.intercept({ handler }) N->>R: Runs async handler (fetch data, render view) R-->>N: Handler promise resolves N->>B: Commits URL, updates history entry else not intercepted N->>B: Falls through to full page navigation end B->>U: Updated view / new document
visualized byIOCombats

event.intercept() is the core mechanism. It tells the browser "I am handling this navigation myself, do not do a full page load." The handler you pass in is an async function, and the browser waits for it to resolve before it considers the navigation complete, which is also what lets it coordinate correctly with the View Transitions API for animated route changes. Compare that to the old approach, where you had to manually call event.preventDefault() on click handlers, guess at loading states, and hope you caught every code path that could change the URL.

The second piece is navigation.entries(), which returns the full list of NavigationHistoryEntry objects in the session history stack, each with a stable key, a url, and arbitrary state you attached to it. This is the part that was simply impossible before: the History API gave you history.length, a number, and nothing else. Now a router can render a proper "you visited these 4 pages" breadcrumb or restore scroll position keyed by entry, not by fragile array indexing.

One caveat worth knowing: Safari 26.2 ships the navigate event and intercept(), but not event.intercept()'s precommitHandler option, which lets you run async work (like fetching data) before the URL bar updates rather than after. If your interception logic depends on that ordering, you need a fallback for Safari until it catches up.

CSS Anchor Positioning: deleting a JavaScript library

Every popover, tooltip, and dropdown library (Popper, Floating UI, and their predecessors) exists to solve one problem: position an element relative to another element, and flip it to the other side if it would overflow the viewport. Doing this in JavaScript means calling getBoundingClientRect() on both elements, computing the delta, writing new top/left values, and re-running all of that on every scroll and resize event, because layout can shift at any time. That is a layout read followed by a style write, repeated continuously. It is exactly the kind of work that causes jank.

CSS Anchor Positioning moves this into the browser's own layout engine, where it belongs. You tag the reference element as an anchor with anchor-name, then reference it from the positioned element using the anchor() function inside its own position properties:

.trigger-button {
  anchor-name: --my-anchor;
}

.tooltip {
  position: absolute;
  position-anchor: --my-anchor;

  /* Position 8px below the anchor's bottom edge, centered on its inline axis */
  top: calc(anchor(bottom) + 8px);
  left: anchor(center);
  translate: -50% 0;

  position-try-fallbacks: flip-block;
}

anchor(bottom) resolves to the anchor element's bottom edge in the positioned element's coordinate space, recalculated automatically by the layout engine whenever either element moves, no JavaScript listener required. position-try-fallbacks: flip-block tells the browser: if this placement would overflow the viewport, try flipping to the other axis (in this case, placing the tooltip above the button instead of below) before falling back to the default. This is the exact "flip middleware" behavior that Popper and Floating UI ship as hundreds of lines of positioning math, now expressed in two CSS declarations.

For more control than the built-in keywords give you, @position-try lets you define named custom fallback positions:

@position-try --above {
  top: auto;
  bottom: calc(anchor(top) + 8px);
}

.tooltip {
  position-try-fallbacks: --above;
}

The practical result: for the large majority of tooltip, popover, and dropdown positioning that used to require a runtime dependency, you can delete that dependency. The bundle gets smaller and the positioning gets more correct, because the browser is recalculating it as part of layout rather than as a requestAnimationFrame loop bolted on afterward.

Trusted Types: closing the DOM XSS sinks

Cross-site scripting through the DOM happens through a specific, well-known set of browser APIs called injection sinks: element.innerHTML, element.outerHTML, document.write(), script.src, and a handful of others. Each of these takes a plain JavaScript string and has the browser interpret it as HTML, a script URL, or executable code. If that string contains attacker-controlled data (a query parameter, a comment someone submitted, a field from an API response you don't fully trust), you have an XSS vulnerability, and no amount of code review catches every call site in a large codebase.

Trusted Types changes the type contract at those sinks. With a Content-Security-Policy header of require-trusted-types-for 'script', the browser stops accepting plain strings at any injection sink. It throws a TypeError instead. The only thing those sinks will accept afterward is a TrustedHTML, TrustedScript, or TrustedScriptURL object, and the only way to create one of those objects is through a policy you define explicitly:

const sanitizerPolicy = trustedTypes.createPolicy('app-sanitizer', {
  createHTML: (rawString) => sanitizeHtml(rawString), // your sanitization function
  createScriptURL: (rawUrl) => {
    const allowed = new URL(rawUrl, location.origin);
    if (allowed.origin !== location.origin) {
      throw new TypeError('Untrusted script origin');
    }
    return allowed.href;
  },
});

// This now works, because it produces a TrustedHTML object
element.innerHTML = sanitizerPolicy.createHTML(userComment);

// This throws a TypeError, because it's a raw string hitting a locked sink
element.innerHTML = userComment;
Diagram
100%
flowchart TD A["Raw string<br/>(attacker-controlled)"] -->|"direct assignment"| S["element.innerHTML<br/>(CSP: require-trusted-types-for 'script')"] S -->|"blocked"| X["TypeError thrown<br/>assignment fails"] A2["Raw string"] --> P["trustedTypes.createPolicy('app-sanitizer', { createHTML })"] P --> San["sanitizeHtml() runs"] San --> T["TrustedHTML object"] T -->|"typed assignment"| S2["element.innerHTML<br/>(same sink)"] S2 -->|"allowed"| OK["Assignment succeeds"] style X fill:#f8d7da,stroke:#c0392b style OK fill:#d4edda,stroke:#2e7d32
visualized byIOCombats

What this buys you is architectural, not just syntactic. Instead of relying on every developer remembering to sanitize every string before it touches innerHTML across a codebase with hundreds of files, you get one policy (or a small, named set of policies) as the single choke point all HTML has to pass through. If a new call site sneaks in a raw string, the browser itself throws at runtime rather than the vulnerability shipping silently. Most frameworks (React, Angular in particular) already avoid raw innerHTML in their default rendering path, but Trusted Types protects the code you write outside the framework: third-party widget embeds, dangerouslySetInnerHTML calls, custom Web Components, and any legacy jQuery-era code still hanging around.

Practical takeaway

If you are shipping a new SPA router or evaluating one, check whether it has adopted the Navigation API yet. It replaces enough of what routers hand-roll that it is worth waiting for or contributing to. For anything currently rendered by a positioning library, audit whether the flip/overflow behavior you need is covered by position-try-fallbacks, since dropping the dependency is close to a pure win. And if your app touches innerHTML anywhere outside a framework's own rendering path, adding a Content-Security-Policy: require-trusted-types-for 'script' header with one narrow, audited policy is one of the highest-leverage security changes you can make in an afternoon.

For interviews, all three are good material because they are current, they map to a real problem (unmanaged navigation state, JS-driven layout thrashing, DOM XSS), and explaining the mechanism, not just naming the API, is exactly what separates a strong answer from a memorized one.

Conclusion

Baseline status is the signal that these three APIs stopped being "watch this space" and became "use this in your next PR." The Navigation API gives routers a real event model and a real history stack. CSS Anchor Positioning moves tooltip and popover math out of JavaScript and into layout. Trusted Types turns DOM XSS prevention from a code review discipline into a browser-enforced contract. None of them are exotic; all three are available today with a version check and a sensible fallback.

Advertisement

Ready to practice?

Test your skills with our interactive UI challenges and build your portfolio.

Start Coding Challenge