Skip to content

Inlined Vue <style> blocks are served untransformed (unscoped, raw v-bind()) by dev server #18333

Open

Description

@spsquared

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&amp;type=style&amp;index=0&amp;scoped=6f4de1fe&amp;lang.css">
.box {
    color: red;
}
</style><style data-vite-dev-id="(big path)/src/components/VueB.vue?vue&amp;type=style&amp;index=0&amp;scoped=bd4d3358&amp;lang.css">
.box {
    color: blue;
}
</style><style data-vite-dev-id="(big path)/src/components/VBind.vue?vue&amp;type=style&amp;index=0&amp;scoped=4c9bd129&amp;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

  1. astro-factory commented on Oct 11, 2026

    @astro-factory
    Contributor

    Triage complete

    • Prepare the workspace: ready
    • Install dependencies: complete
    • Build the project: complete
    • Reproduce the issue: confirmed
    • Diagnose the cause: high confidence
    • Verify the diagnosis: bug
    • Attempt a fix: verified
    • Publish the result: set triage: fix pending

    • Reproduced: Yes
    • Exploration: Yes — View branch
    • Unit Test: Yes — packages/integrations/vue/test/basics.test.ts (a dev-server integration test, not a unit test)
    • Priority: P3: minor bug. This only happens in 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. The transform hook that fills this cache is in packages/astro/src/vite-plugin-css/index.ts. Astro's own Vite plugins are listed before plugins added by integrations, so this hook runs before vite:vue. It saves the raw SFC <style> block before @vitejs/plugin-vue adds the [data-v-xxx] scoping and turns v-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.ts adds it after all user and integration Vite config, so it saves the CSS after framework plugins have changed it and before vite:css-post turns 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:

    # astro
    npm i https://pkg.pr.new/astro@37734c7
    # @astrojs/vue
    npm i https://pkg.pr.new/@astrojs/vue@37734c7

    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/vue v7.0.3.
    Environment (triage): Linux sandbox, Node (repo default), Astro 7.3.8 and @astrojs/vue 7.0.3 from the monorepo workspace.

    Steps:

    1. Clone the StackBlitz repro. It has three Vue components with <style scoped>: two that both style .box (red and blue), and one that uses width: v-bind("wide ? '400px' : '100px'").
    2. Run 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);...}
    3. Run astro dev, then curl /. The inline styles are unscoped and untransformed:
    <style data-vite-dev-id=".../VueA.vue?vue&amp;type=style&amp;index=0&amp;scoped=6f4de1fe&amp;lang.css">
    .box { color: red; }
    </style>
    <style data-vite-dev-id=".../VBind.vue?vue&amp;type=style&amp;index=0&amp;scoped=4c9bd129&amp;lang.css">
    .box { background-color: green; width: v-bind("wide ? '400px' : '100px'"); }
    </style>
    1. Fetch the same style module from Vite (/src/components/VueA.vue?vue&type=style&index=0&scoped=6f4de1fe&lang.css). It returns the correct .box[data-v-6f4de1fe] and var(--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: the transform hook of virtual:astro:dev-css runs cssContentCache.set(id, code) with default (normal) order.
    • packages/astro/src/core/create-vite.ts: astroDevCssPlugin is in the core plugin list. Integration plugins (vite:vue) are merged in after it with vite.mergeConfig, so they run later.
    • vite-plugin-app/environment.ts (getScriptsAndStyles) and core/environment/dev-nonrunnable.ts read that cache through virtual:astro:dev-css-all and write it out as <style data-vite-dev-id>.
    • content/vite-plugin-content-assets.ts also reads the same cache.
    • .astro styles are not affected because the compiler scopes them during load. Builds are not affected because the hook does nothing during a build.
    • transform.order: 'post' would not work. It runs after vite:css-post, which has already turned CSS into JS (export {} for SSR).

    Debug log (removed afterwards):

    [DEBUG] env ssr plugins: vite:css,virtual:astro:dev-css,virtual:astro:dev-css-all,vite:vue,...,vite:css-post code: "\r\n.box {\r\n  color: red;\r\n}\r\n"
    

    Verification

    Verdict: bug (high confidence).

    • The source says what it intends: "we still want to inject the styles to avoid FOUC. The style tags should emulate what Vite injects so further HMR works as expected."
    • No comment, guard or test acknowledges that integration plugins run after the caching hook.
    • Build output is correct, so dev and build disagree.
    • The cache was introduced in the Environment API refactor (Environment API #14306).

    Fix

    • vite-plugin-css/index.ts: the cache transform hook moves out of astroDevCssPlugin into a new exported astroDevCssContentCachePlugin({ cssContentCache }). It keeps the same filter and environments.
    • create-vite.ts: when command === '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 before vite:css-post.

    Tests:

    • Repro: the dev HTML now has .box[data-v-6f4de1fe], .box[data-v-bd4d3358] and width: var(--4c9bd129-wide…). Editing a component's style while the server runs updates the inline CSS on the next request.
    • New test: a dev test in packages/integrations/vue/test/basics.test.ts, inlines Vue scoped styles transformed by the Vue plugin. It uses new fixture files ScopedStyle.vue and scoped-style.astro. It fails without the fix and passes with it.
    • Updated test: packages/astro/test/0-css.test.ts asserted the buggy dev output .vue-scoped{. It now asserts .vue-scoped[data-v-…]{, the same as the existing build assertion.
    • Regression runs:
      • Astro css-*, content-collection*, head-propagation*, client-only-child-styles and glob-pages-css tests: 112/112 pass.
      • 0-css passes.
      • Astro CSS unit tests: 50/50 pass.
      • Vue integration suite: 27/27 pass.
      • Type-check, Biome and ESLint are clean.

    Changeset: .changeset/vue-dev-scoped-styles.md (astro: patch).

    Limitation: a plugin that another plugin adds from inside its own config hook could still end up running after the cache step. That's rare.

    --- a/packages/astro/src/core/create-vite.ts
    +++ b/packages/astro/src/core/create-vite.ts
    -import { astroDevCssPlugin } from '../vite-plugin-css/index.js';
    +import { astroDevCssContentCachePlugin, astroDevCssPlugin } from '../vite-plugin-css/index.js';
    @@
     	result = vite.mergeConfig(result, commandConfig);
    +	if (command === 'dev') {
    +		// Appended last so it sees CSS after integration plugins have transformed it.
    +		result = vite.mergeConfig(result, {
    +			plugins: [astroDevCssContentCachePlugin({ cssContentCache })],
    +		});
    +	}
    
    --- a/packages/astro/src/vite-plugin-css/index.ts
    +++ b/packages/astro/src/vite-plugin-css/index.ts
    -			transform: { ... cssContentCache.set(id, code) ... },   // removed from astroDevCssPlugin
    +export function astroDevCssContentCachePlugin({
    +	cssContentCache,
    +}: Pick<AstroVitePluginOptions, 'cssContentCache'>): Plugin {
    +	return {
    +		name: 'astro:dev-css-content-cache',
    +		applyToEnvironment(env) { /* astro, ssr, client, prerender */ },
    +		transform: {
    +			filter: { id: { include: [CSS_LANGS_RE], exclude: [rawRE, inlineRE] } },
    +			handler(code, id) {
    +				const mod = (this.environment as DevEnvironment).moduleGraph.getModuleById(id);
    +				if (mod) {
    +					cssContentCache.set(id, code);
    +				}
    +			},
    +		},
    +	};
    +}

    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.

  2. changed the title [-]Vue style is not transformed by Vite during Astro dev[/-] [+]Inlined Vue <style> blocks are served untransformed (unscoped, raw v-bind()) by dev server[/+] on Oct 11, 2026
  3. added
    - P3: minor bugAn edge case that only affects very specific usage (priority)
    pkg: astroRelated to the core `astro` package (scope)
    pkg: vueRelated to Vue (scope)
    triage: fix pendingReporter needs to verify the triage bot fix works
    and removed on Oct 11, 2026
  4. spsquared commented on Oct 11, 2026

    @spsquared
    Author

    Yep, that was the same diagnosis I found locally and the fix works.

  5. added
    triage: fix verifiedReporter confirmed the triage bot fix works
    and removed
    triage: fix pendingReporter needs to verify the triage bot fix works
    on Oct 11, 2026
  6. astro-factory commented on Oct 11, 2026

    @astro-factory
    Contributor

    The fix has been verified! A pull request has been created: #18336

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

- P3: minor bugAn edge case that only affects very specific usage (priority)pkg: astroRelated to the core `astro` package (scope)pkg: vueRelated to Vue (scope)triage: fix verifiedReporter confirmed the triage bot fix works

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions