diff --git a/.changeset/rotten-cups-happen.md b/.changeset/rotten-cups-happen.md
new file mode 100644
index 000000000..e484c098f
--- /dev/null
+++ b/.changeset/rotten-cups-happen.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Fix duplicate CSS in dev mode when `vite.css.devSourcemap` is provided
diff --git a/packages/astro/e2e/css-sourcemaps.test.js b/packages/astro/e2e/css-sourcemaps.test.js
new file mode 100644
index 000000000..07cea4cb0
--- /dev/null
+++ b/packages/astro/e2e/css-sourcemaps.test.js
@@ -0,0 +1,38 @@
+import { expect } from '@playwright/test';
+import { getColor, isWindows, testFactory } from './test-utils.js';
+
+const test = testFactory({
+ root: './fixtures/css/',
+});
+
+let devServer;
+
+test.beforeAll(async ({ astro }) => {
+ devServer = await astro.startDevServer();
+});
+
+test.afterAll(async () => {
+ await devServer.stop();
+});
+
+test.describe('CSS Sourcemap HMR', () => {
+ test.skip(isWindows, 'TODO: fix css hmr in windows');
+
+ test('removes Astro-injected CSS once Vite-injected CSS loads', async ({ page, astro }) => {
+ const html = await astro.fetch('/').then(res => res.text());
+
+ // style[data-astro-dev-id] should exist in initial SSR'd markup
+ expect(html).toMatch('data-astro-dev-id');
+
+ await page.goto(astro.resolveUrl('/'));
+
+ // Ensure JS has initialized
+ await page.waitForTimeout(500);
+
+ // style[data-astro-dev-id] should NOT exist once JS runs
+ expect(await page.locator('style[data-astro-dev-id]').count()).toEqual(0);
+
+ // style[data-vite-dev-id] should exist now
+ expect(await page.locator('style[data-vite-dev-id]').count()).toBeGreaterThan(0);
+ });
+});
diff --git a/packages/astro/e2e/css.test.js b/packages/astro/e2e/css.test.js
index b817c419a..745a540ee 100644
--- a/packages/astro/e2e/css.test.js
+++ b/packages/astro/e2e/css.test.js
@@ -30,4 +30,22 @@ test.describe('CSS HMR', () => {
expect(await getColor(h)).toBe('rgb(0, 128, 0)');
});
+
+ test('removes Astro-injected CSS once Vite-injected CSS loads', async ({ page, astro }) => {
+ const html = await astro.fetch('/').then(res => res.text());
+
+ // style[data-astro-dev-id] should exist in initial SSR'd markup
+ expect(html).toMatch('data-astro-dev-id');
+
+ await page.goto(astro.resolveUrl('/'));
+
+ // Ensure JS has initialized
+ await page.waitForTimeout(500);
+
+ // style[data-astro-dev-id] should NOT exist once JS runs
+ expect(await page.locator('style[data-astro-dev-id]').count()).toEqual(0);
+
+ // style[data-vite-dev-id] should exist now
+ expect(await page.locator('style[data-vite-dev-id]').count()).toBeGreaterThan(0);
+ });
});
diff --git a/packages/astro/e2e/fixtures/css-sourcemaps/astro.config.mjs b/packages/astro/e2e/fixtures/css-sourcemaps/astro.config.mjs
new file mode 100644
index 000000000..7e8fac1e7
--- /dev/null
+++ b/packages/astro/e2e/fixtures/css-sourcemaps/astro.config.mjs
@@ -0,0 +1,7 @@
+export default {
+ vite: {
+ css: {
+ devSourcemap: true,
+ }
+ }
+};
diff --git a/packages/astro/e2e/fixtures/css-sourcemaps/package.json b/packages/astro/e2e/fixtures/css-sourcemaps/package.json
new file mode 100644
index 000000000..1fa4c2c79
--- /dev/null
+++ b/packages/astro/e2e/fixtures/css-sourcemaps/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@e2e/css-sourcemaps",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "astro": "workspace:*"
+ }
+}
diff --git a/packages/astro/e2e/fixtures/css-sourcemaps/src/env.d.ts b/packages/astro/e2e/fixtures/css-sourcemaps/src/env.d.ts
new file mode 100644
index 000000000..8c34fb45e
--- /dev/null
+++ b/packages/astro/e2e/fixtures/css-sourcemaps/src/env.d.ts
@@ -0,0 +1 @@
+///