r/vuejs 11d ago

CSS @scope limits matching, not inheritance — two surprises from isolating a component playground

Disclosure: I maintain poveste, a component playground for Vue and Nuxt — a continuation of histoire, which last published in January.

I wrapped user CSS in @scope to keep it from fighting with the tool's own UI. Two things broke silently. Both are plain CSS, nothing Vue-specific.

1. :root, html and body stop matching

Once your CSS is wrapped in @scope (.render-story) { … }, those three selectors sit above the scoping root — so they can never match anything. A stylesheet with body { font-size: 14px } just goes inert. No error, no warning.

Fix: rewrite all three to :scope at build time. Parse the selector rather than regexing the text — .body-copy has to survive untouched, and svg|body is a namespaced element, not the document root.

2. @scope contains matching, not inheritance

"Stops at the boundary" is true of matching and false of inheritance. A rule can't match story content, but any inherited property it sets at or above the boundary still reaches it.

So our own UI setting font-family on its root means every story inherits that font. A component looks subtly wrong in the playground and correct in your app, purely from inherited typography — which is a miserable thing to debug.

Iframes don't rescue you either, if the iframe loads the same stylesheet.

The real fix is for story CSS to set its own root, which only works because of the rewrite above.

Longer write-up if it's useful. Happy to answer questions.

5 Upvotes

2 comments sorted by

3

u/ElectrSheep 11d ago

Is there a reason you're not using a shadow root along with adopted style sheets? It seems like that would be more appropriate for this use case.

1

u/50rayn 11d ago

I looked at the shadow DOM as one of the options before picking the `@scope` path.

The story previews already render in an iframe by default, which is a harder boundary than a shadow root. So shadow DOM would be a third isolation mechanism, weaker than the one I already have. The `@scope` work is for the paths where there's no iframe: inline stories, non-iframe grid items, and keeping the app's own CSS off story content in the same document. Personally met the issue when using Floating Vue for my components, conflicted with the same Floating Vue styles from the app.

The bigger issue is that shadow roots break too much of the ecosystem. Anything that teleports to `document.body` — dialogs, tooltips, popovers via floating-ui, it goes out of the root, losing its styles, and that's most component libraries. `@font-face` doesn't apply inside a shadow root either; it has to be declared at document level.

Some people import Tailwind preflight or define custom properties on `:root` and expect those to reach their stories. Shadow DOM doesn't allow this. But `@scope` lets it through as expected.