All blogs

Chrome's Modern Web Guidance


Contents
  1. What it is
  2. Adding it to a project
  3. Tell it which browsers you care about
  4. Four transitions on this site
  5. Where the guides overruled my code
  6. Checking it worked
  7. Keeping it installed

Coding agents write CSS from whatever was common in their training data, and a lot of that predates @starting-style, view transitions and scroll-driven animations. Ask for a fade-in and you're likely to get a useEffect toggling a class after mount, or a transition: all that animates things you never meant to move.

The Chrome team now packages its best practice guides for more than 100 web use cases as Modern Web Guidance, a skill your agent reads before it writes frontend code. I added it to this site, a Next.js app, and used it to build four CSS transitions. Along the way the guides flagged two things in CSS I'd had for over a year.

What it is

Modern Web Guidance is a catalog of guides with a small CLI in front of it. Each guide covers one use case: animating a dialog, a scroll progress bar, a passkey sign-in, an autofilled address form. It gives the implementation, the gotchas, browser support pulled from Baseline data, and what to do in browsers that lack the feature.

You can search and read the guides yourself:

npx -y modern-web-guidance@latest search "view transitions between pages"
npx -y modern-web-guidance@latest retrieve "directional-navigation-transitions"

Search returns JSON with guide IDs, descriptions and token counts, and retrieve prints the guide as markdown. The skill wraps those two commands in instructions for a coding agent: search with a short query before writing frontend code, retrieve the matching guide, and check the result against it before calling the job done.

Chrome says each use case has Playwright evals behind it, and that early results showed a 37 percentage point improvement in how closely agents followed modern best practices. I haven't checked that number. The guides I read were specific, down to which declaration has to come second and why.

Adding it to a project

One command from the project root:

npx modern-web-guidance@latest install

It cloned the repo, put the skill in .agents/skills/modern-web-guidance, and wrote a skills-lock.json recording its source and hash. .agents/skills is the shared location that Amp, Cline, Codex and a dozen or so other agents read, and the installer also links it into .claude/skills for Claude Code. Commit the folder and everyone working on the repo, with whichever agent, gets the same guides. npx modern-web-guidance@latest update pulls new ones.

The installer runs a few security scanners on the way in. One rated the skill "Critical Risk", which is a jarring thing to see from a Google project. I read the files before using them and found only markdown, with no scripts or hooks. My guess is the scanner flags any skill that tells an agent to run npx. Read anything you install into an agent that can run shell commands, though, whoever it comes from.

The CLI also sends anonymous telemetry to Google. DISABLE_TELEMETRY=1 in your shell profile turns it off.

Tell it which browsers you care about

By default the guides assume you only want features that are Baseline Widely available, meaning they've worked in every major browser for at least 30 months. Anything newer comes with a fallback, sometimes a JavaScript one.

That's the wrong default for a portfolio site, where a missing animation costs nothing. So the project's agent instructions file (AGENTS.md, or CLAUDE.md in this repo) now has one paragraph:

Browser Support: Baseline Widely available features are used freely. Baseline Newly available and Limited availability features (view transitions, @starting-style, scroll-driven animations) are allowed only as progressive enhancement: feature-detect them, and the page must work, unanimated, without them. Decorative motion gets no polyfills or JS fallbacks.

The agent reads this and uses each guide's Baseline dates to decide which fallbacks to skip. Without it you get fallback code you don't need.

Four transitions on this site

Each of these is live on this site now, and two of them run on this page: the slide and title morph when you open it from the blog index, and the progress bar along the top.

1. Page transitions that know which way you're going

The site already had view transitions between the chat, the blog index and each post. A TransitionLink component worked out whether you were going deeper or coming back, and set a data-nav attribute on <html> so the CSS could slide the right way. It worked, but it meant setting attributes and cleaning them up after every navigation.

The directional-navigation-transitions guide does this with view transition types. You pass the direction when starting the transition:

document.startViewTransition({ update, types: ["forward"] });

and match it in CSS:

html:active-view-transition-type(forward, back)::view-transition-new(root) {
  animation: var(--vt-duration) var(--vt-ease) both vt-page-in;
}

A type only exists while its transition runs, so there's nothing to clean up afterwards. Moving between two blog pages adds a blog-shared type, which is what lets a post's title morph from its row on the index into the heading of the post.

Typed transitions are newer than view transitions themselves, so a small helper checks for types support and returns null when it's missing:

if (
  typeof ViewTransition === "undefined" ||
  !("types" in ViewTransition.prototype)
) {
  return null;
}

That makes one thing worse. Chrome before 125, Safari before 18.2 and Firefox before 147 used to get the slide and now navigate without it. In October 2026 that's a small group, and they still land on the right page.

2. The chat opening up

The home page starts with a centred question and an input bar. Once the first answer comes back, the layout switches to a conversation with the input docked at the bottom. That used to be a jump between two unrelated layouts.

Now it's a view transition typed chat-open. The input bar gets its own view-transition-name, so the browser moves it from the middle of the screen to the bottom while the rest of the page cross-fades.

React needs one extra step. The browser snapshots the old page, calls your update callback, then snapshots the new one. React normally batches state updates and renders whenever it likes, which might be after that second snapshot. flushSync forces the commit to happen inside the callback:

startTypedTransition(
  () => flushSync(() => setHasSearched(true)),
  ["chat-open"]
);

The voice mode screen has no input bar to move, and the site's own reduce motion toggle should be respected, so both skip the transition and update straight away.

3. Messages that fade in

New chat bubbles now rise a few pixels and fade in when they appear. No JavaScript is involved:

.chat-message {
  transition:
    opacity 0.3s ease-out,
    translate 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}

@starting-style {
  .chat-message {
    opacity: 0;
    translate: 0 10px;
  }
}

The animate-element-entry-exit guide explains why @starting-style is needed: browsers skip the transition on an element's first style, so without a "before" state there's nothing to animate from. The skill adds that @starting-style blocks should stay at the top level of the stylesheet, not nested inside a component's rule.

The notice banner got the same treatment. It's centred with transform: translateX(-50%), and the entry animation uses the separate translate property. The two combine, so the animation never knocks the banner off centre.

4. A reading progress bar on posts

Scroll down this page. The thin line along the top is a scroll-driven animation, with no scroll listener:

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: scroll()) {
    .blog-progress {
      animation: blog-progress-grow auto linear both;
      animation-timeline: scroll(root block);
    }
  }
}

The scroll-progress-indicator guide flags an ordering trap: animation-timeline has to come after the animation shorthand, because the shorthand resets it. Swap those two lines and the animation falls back to the normal time-based timeline with an auto duration, so the bar sits at full width wherever you are on the page.

Firefox doesn't support scroll-driven animations yet. The guide says that a purely decorative effect should get no fallback, so Firefox readers don't see the bar. The element is also aria-hidden, so screen readers skip an empty div that means nothing to them.

Where the guides overruled my code

The transition: all 0.5s ease-in-out on the chat input bar and its container had been there since May 2025, when I first wired up the chat. The guides warn against transition: all for performance, but here it also broke something. A view transition's new snapshot is live, so any CSS transition still running inside it plays at the same time. The bar would have been moved by the view transition and also had its width and margins eased by CSS, two animations fighting over one element. It now only transitions background-color, for theme changes.

The reduced motion rule I added during an accessibility pass in June 2025 is also on the guides' wrong list, and I haven't changed it yet:

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

The motion guide says not to apply this globally, because some animations get more jarring at near-zero duration, and to handle reduced motion per animation instead. Everything new in this post does that. The old rule is next on the list.

Checking it worked

A view transition that silently doesn't run looks exactly like a page that never had one. So I wrapped document.startViewTransition in the dev server's browser and log each transition's types and every animation on its pseudo-elements.

The first chat answer logged a chat-open transition with a 480ms group for chat-input. Going from home to the blog logged forward, with the background canvas fading on its own layer instead of sliding. Opening a post logged forward and blog-shared, with the header and the post title each in their own group. Scrolled to 64% of a post, the progress bar's transform read scaleX(0.643).

There was one false alarm. The first screenshot froze mid cross-fade, which looked like a stuck transition. The browser pane was hidden, so the page was being throttled as a background tab. A couple of seconds later the transition had finished.

Keeping it installed

The skill stays in the repo. The four transitions took one session, and the better result was the review of what was already there: the transition: all I'd stopped seeing, and a reduced motion rule that turned out to be the pattern the guides tell you to avoid. Every guide puts the browser support numbers next to the code, so deciding to drop a fallback is a decision you make on purpose.

If you want to try it, run npx modern-web-guidance@latest install, write your browser support policy into AGENTS.md, and ask your agent for something you'd normally build with a useEffect.