Repository navigation
Inlined Vue <style> blocks are served untransformed (unscoped, raw v-bind()) by dev server #18333
Description
Astro Info
Astro v7.3.8
Vite v8.3.4
Node v24.11.1
System Windows (x64)
Package Manager npm
Output static
Adapter none
Integrations @astrojs/vue (v7.0.3)
If this issue only occurs in one browser, which browser is a problem?
No response
Describe the Bug
When using astro dev server, emitted Vue CSS in the initial HTML is not the version that was transformed by Vite, dropping style scoping and leaving raw v-bind strings in CSS. I suspect that the server-rendered <style> was captured before @vitejs/plugin-vue could transform it.
This causes collisions between components and broken initial states. In the attached reproduction, astro dev outputs unscoped CSS at first, and a subsequent script that Astro adds in for Vite HMR fixes it. I believe the inline CSS is meant to be identical to what the Vite transform would swap in based on this comment in vite-plugin-app/environment.js:
The style tags should emulate what Vite injects so further HMR works as expected
A build using astro build does not do this, and SSR using a Node adapter work too, so this is dev-only.
I think this is similar to issue #17312, but is a different cause. It's also close to #16334, but that was closed only for ClientRouter and is still a different root cause.
Here's a sample of the HTML produced by the reproduction:
<style data-vite-dev-id="(big path)/src/components/VueA.vue?vue&type=style&index=0&scoped=6f4de1fe&lang.css">
.box {
color: red;
}
</style><style data-vite-dev-id="(big path)/src/components/VueB.vue?vue&type=style&index=0&scoped=bd4d3358&lang.css">
.box {
color: blue;
}
</style><style data-vite-dev-id="(big path)/src/components/VBind.vue?vue&type=style&index=0&scoped=4c9bd129&lang.css">
.box {
background-color: green;
width: v-bind("wide ? '400px' : '100px'");
}
</style><script type="module" src="/@vite/client"></script><!-- more scripts --><div class="box" data-v-6f4de1fe>Vue A (should be red)</div><div class="box" data-v-bd4d3358>Vue B (should be blue)</div><div class="box" data-v-4c9bd129>Narrow</div></body></html>In the reproduction, the flash is usually not visible, but with larger sites astro dev sometimes produces meaningful collisions and invalid CSS that causes spurious layout shifts while the page loads that disappear when actually built. If testing for JavaScript disabled behavior, this can also create the illusion of bugs.
What's the expected result?
astro dev should use the transformed CSS for Vue components in the initial HTML, identical to the subsequent Vite transformed CSS.
Link to Minimal Reproducible Example
https://stackblitz.com/edit/astro-dev-vue-bug-repro-1?file=src%2Fpages%2Findex.astro
Participation
- I am willing to submit a pull request for this issue.
Activity
astro-factory commentedon Oct 11, 2026
Triage complete
triage: fix pendingpackages/integrations/vue/test/basics.test.ts(a dev-server integration test, not a unit test)astro dev, and Vite's client script replaces the styles with the correct ones once JS loads. Build and SSR output are fine. The effect is a temporary flash of unscoped styles, and broken styles if JS is disabled.During dev, Astro saves each CSS file's contents in a cache (
cssContentCache) and uses it for the inline<style data-vite-dev-id>tags. Thetransformhook that fills this cache is inpackages/astro/src/vite-plugin-css/index.ts. Astro's own Vite plugins are listed before plugins added by integrations, so this hook runs beforevite:vue. It saves the raw SFC<style>block before@vitejs/plugin-vueadds the[data-v-xxx]scoping and turnsv-bind()into CSS variables. A debug log of the order confirmed it:vite:css → virtual:astro:dev-css (caches here) → vite:vue → vite:css-post.I found a potential fix for this issue. The caching hook moves into a small dev-only plugin,
astroDevCssContentCachePlugin.create-vite.tsadds it after all user and integration Vite config, so it saves the CSS after framework plugins have changed it and beforevite:css-postturns it into JS. With the fix, the inline dev styles match what Vite's client script injects. This needs human review.Try this fix
You can test this fix right now without waiting for a release:
If this fixes your issue, please leave a comment letting us know (for example
"confirmed, this fixes it"). A pull request will then be opened to get it merged.
Full Triage Report
Reproduction
Environment (reported): Astro v7.3.8, Vite v8.3.4, Node v24.11.1, Windows x64, npm, static output,
@astrojs/vuev7.0.3.Environment (triage): Linux sandbox, Node (repo default), Astro 7.3.8 and
@astrojs/vue7.0.3 from the monorepo workspace.Steps:
<style scoped>: two that both style.box(red and blue), and one that useswidth: v-bind("wide ? '400px' : '100px'").astro build. The output is correct:.box[data-v-4556c76c]{color:red}.box[data-v-1f2a38ec]{color:#00f}.box[data-v-5ca3ed3f]{width:var(--d1ee6f0a);...}astro dev, thencurl /. The inline styles are unscoped and untransformed:/src/components/VueA.vue?vue&type=style&index=0&scoped=6f4de1fe&lang.css). It returns the correct.box[data-v-6f4de1fe]andvar(--4c9bd129-wide…). So only the HTML that Astro renders on the server has the raw CSS.Expected: The inline dev CSS should be identical to the Vite-transformed CSS.
Actual: The raw
<style>block is inlined. Vite's client script fixes it later, which causes a flash and broken styling when JS is disabled.Diagnosis
packages/astro/src/vite-plugin-css/index.ts: thetransformhook ofvirtual:astro:dev-cssrunscssContentCache.set(id, code)with default (normal) order.packages/astro/src/core/create-vite.ts:astroDevCssPluginis in the core plugin list. Integration plugins (vite:vue) are merged in after it withvite.mergeConfig, so they run later.vite-plugin-app/environment.ts(getScriptsAndStyles) andcore/environment/dev-nonrunnable.tsread that cache throughvirtual:astro:dev-css-alland write it out as<style data-vite-dev-id>.content/vite-plugin-content-assets.tsalso reads the same cache..astrostyles are not affected because the compiler scopes them duringload. Builds are not affected because the hook does nothing during a build.transform.order: 'post'would not work. It runs aftervite:css-post, which has already turned CSS into JS (export {}for SSR).Debug log (removed afterwards):
Verification
Verdict: bug (high confidence).
Fix
vite-plugin-css/index.ts: the cachetransformhook moves out ofastroDevCssPlugininto a new exportedastroDevCssContentCachePlugin({ cssContentCache }). It keeps the same filter and environments.create-vite.ts: whencommand === 'dev', that plugin is merged in after the user, integration and command config. That makes it the last normal-order plugin, so it runs just beforevite:css-post.Tests:
.box[data-v-6f4de1fe],.box[data-v-bd4d3358]andwidth: var(--4c9bd129-wide…). Editing a component's style while the server runs updates the inline CSS on the next request.packages/integrations/vue/test/basics.test.ts,inlines Vue scoped styles transformed by the Vue plugin. It uses new fixture filesScopedStyle.vueandscoped-style.astro. It fails without the fix and passes with it.packages/astro/test/0-css.test.tsasserted the buggy dev output.vue-scoped{. It now asserts.vue-scoped[data-v-…]{, the same as the existing build assertion.css-*,content-collection*,head-propagation*,client-only-child-stylesandglob-pages-csstests: 112/112 pass.0-csspasses.Changeset:
.changeset/vue-dev-scoped-styles.md(astro: patch).Limitation: a plugin that another plugin adds from inside its own
confighook could still end up running after the cache step. That's rare.This report was made by an LLM. The analysis may be wrong, and the potential fix might not work, but is intended as a starting point for exploring the issue.
[-]Vue style is not transformed by Vite during Astro dev[/-][+]Inlined Vue <style> blocks are served untransformed (unscoped, raw v-bind()) by dev server[/+]spsquared commentedon Oct 11, 2026
Yep, that was the same diagnosis I found locally and the fix works.
astro-factory commentedon Oct 11, 2026
The fix has been verified! A pull request has been created: #18336