← /journal

May 19, 2026 · 3 min

The Astro CSS scoping gotcha that ate my afternoon

Twelve custom view transitions, all looking identical. Took an embarrassing amount of time to spot why.

We built a /lab/view-transitions demo with twelve different animations. Flip, slide, iris, glitch, all twelve of them, each with its own keyframes.

Every transition looked the same. A simple cross-fade. No flip. No slide. No glitch.

We checked the keyframes. They were right. We checked the class toggle on <html> — firing correctly. We checked the animation names. Matched. We added console.logs. Everything looked like it was working.

Half an hour in, we opened DevTools’ Computed tab. The animation property was set to:

animation: -ua-view-transition-fade-out 250ms;

The browser default. None of our twelve custom keyframes were applying.

Astro scopes the styles in every <style> block. It adds a [data-astro-cid-XXX] attribute to each selector and to every element the component renders. That’s how component styles stay isolated. Most of the time you don’t think about it.

Our selector:

.vt-style-flip-y ::view-transition-old(vt-morph) { ... }

Compiled to:

.vt-style-flip-y[data-astro-cid-XXX] ::view-transition-old(vt-morph)[data-astro-cid-XXX]

::view-transition-* pseudo-elements are attached to the document root by the browser. They live outside Astro’s component tree. They carry no data-astro-cid-anything. So every one of our twelve rules failed silently and the browser default ran instead.

Two fixes:

<style is:global>
  html.vt-style-flip-y::view-transition-old(vt-morph) { ... }
</style>

is:global opts the block out of scoping. And the selector form changes — direct pseudo (html.X::view-transition-old(...)), not the descendant combinator (.X ::view-transition-old(...)). The pseudo-element isn’t really a descendant.

What we keep thinking about: Astro’s scoping is normally invisible. The compiled HTML works. The styles apply. You forget the mechanism exists.

Until you hit something the scoping can’t see. View-transition pseudos. Third-party widgets. Shadow DOM. Then you’re staring at code that looks right and does nothing, and the debugger isn’t going to help because the styles are there, they just don’t reach the target.

The lesson is small. If a CSS rule in Astro appears to do nothing, the first question to ask isn’t “is the syntax right” — it’s “can this selector even reach the target through the scoping attributes?”

If no, is:global, or a separate global stylesheet, or move on.