diff --git a/integrations/postcss/multi-root.test.ts b/integrations/postcss/multi-root.test.ts
new file mode 100644
index 000000000..bfd2340e5
--- /dev/null
+++ b/integrations/postcss/multi-root.test.ts
@@ -0,0 +1,50 @@
+import { candidate, css, html, js, json, test } from '../utils'
+
+test(
+ 'production build',
+ {
+ fs: {
+ 'package.json': json`
+ {
+ "dependencies": {
+ "postcss": "^8",
+ "postcss-cli": "^10",
+ "tailwindcss": "workspace:^",
+ "@tailwindcss/postcss": "workspace:^"
+ }
+ }
+ `,
+ 'postcss.config.js': js`
+ module.exports = {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ },
+ }
+ `,
+ 'index.html': html`
+
+ `,
+ 'src/shared.css': css`
+ @import 'tailwindcss/theme' theme(reference);
+ @import 'tailwindcss/utilities';
+ `,
+ 'src/root1.css': css`
+ @import './shared.css';
+ @variant one (&:is([data-root='1']));
+ `,
+ 'src/root2.css': css`
+ @import './shared.css';
+ @variant two (&:is([data-root='2']));
+ `,
+ },
+ },
+ async ({ fs, exec }) => {
+ await exec('pnpm postcss src/*.css -d dist')
+
+ await fs.expectFileToContain('dist/root1.css', [candidate`one:underline`])
+ await fs.expectFileNotToContain('dist/root1.css', [candidate`two:underline`])
+
+ await fs.expectFileNotToContain('dist/root2.css', [candidate`one:underline`])
+ await fs.expectFileToContain('dist/root2.css', [candidate`two:underline`])
+ },
+)
diff --git a/integrations/vite/multi-root.test.ts b/integrations/vite/multi-root.test.ts
new file mode 100644
index 000000000..76b189d9c
--- /dev/null
+++ b/integrations/vite/multi-root.test.ts
@@ -0,0 +1,164 @@
+import { expect } from 'vitest'
+import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts } from '../utils'
+
+test(
+ `production build`,
+ {
+ fs: {
+ 'package.json': json`
+ {
+ "type": "module",
+ "dependencies": {
+ "@tailwindcss/vite": "workspace:^",
+ "tailwindcss": "workspace:^"
+ },
+ "devDependencies": {
+ "vite": "^5.3.5"
+ }
+ }
+ `,
+ 'vite.config.ts': ts`
+ import tailwindcss from '@tailwindcss/vite'
+ import path from 'node:path'
+ import { defineConfig } from 'vite'
+
+ export default defineConfig({
+ build: {
+ cssMinify: false,
+ rollupOptions: {
+ input: {
+ root1: path.resolve(__dirname, 'root1.html'),
+ root2: path.resolve(__dirname, 'root2.html'),
+ },
+ },
+ },
+ plugins: [tailwindcss()],
+ })
+ `,
+ 'root1.html': html`
+
+
+
+
+ Hello, world!
+
+ `,
+ 'src/shared.css': css`
+ @import 'tailwindcss/theme' theme(reference);
+ @import 'tailwindcss/utilities';
+ `,
+ 'src/root1.css': css`
+ @import './shared.css';
+ @variant one (&:is([data-root='1']));
+ `,
+ 'root2.html': html`
+
+
+
+
+ Hello, world!
+
+ `,
+ 'src/root2.css': css`
+ @import './shared.css';
+ @variant two (&:is([data-root='2']));
+ `,
+ },
+ },
+ async ({ fs, exec }) => {
+ await exec('pnpm vite build')
+
+ let files = await fs.glob('dist/**/*.css')
+ expect(files).toHaveLength(2)
+
+ let root1 = files.find(([filename]) => filename.includes('root1'))
+ let root2 = files.find(([filename]) => filename.includes('root2'))
+
+ expect(root1).toBeDefined()
+ expect(root2).toBeDefined()
+
+ expect(root1![1]).toContain(candidate`one:underline`)
+ expect(root1![1]).not.toContain(candidate`two:underline`)
+
+ expect(root2![1]).not.toContain(candidate`one:underline`)
+ expect(root2![1]).toContain(candidate`two:underline`)
+ },
+)
+
+test(
+ `dev mode`,
+ {
+ fs: {
+ 'package.json': json`
+ {
+ "type": "module",
+ "dependencies": {
+ "@tailwindcss/vite": "workspace:^",
+ "tailwindcss": "workspace:^"
+ },
+ "devDependencies": {
+ "vite": "^5.3.5"
+ }
+ }
+ `,
+ 'vite.config.ts': ts`
+ import tailwindcss from '@tailwindcss/vite'
+ import path from 'node:path'
+ import { defineConfig } from 'vite'
+
+ export default defineConfig({
+ build: { cssMinify: false },
+ plugins: [tailwindcss()],
+ })
+ `,
+ 'root1.html': html`
+
+
+
+
+ Hello, world!
+
+ `,
+ 'src/shared.css': css`
+ @import 'tailwindcss/theme' theme(reference);
+ @import 'tailwindcss/utilities';
+ `,
+ 'src/root1.css': css`
+ @import './shared.css';
+ @variant one (&:is([data-root='1']));
+ `,
+ 'root2.html': html`
+
+
+
+
+ Hello, world!
+
+ `,
+ 'src/root2.css': css`
+ @import './shared.css';
+ @variant two (&:is([data-root='2']));
+ `,
+ },
+ },
+ async ({ root, spawn, getFreePort, fs }) => {
+ let port = await getFreePort()
+ await spawn(`pnpm vite dev --port ${port}`)
+
+ // Candidates are resolved lazily, so the first visit of index.html
+ // will only have candidates from this file.
+ await retryAssertion(async () => {
+ let styles = await fetchStyles(port, '/root1.html')
+ expect(styles).toContain(candidate`one:underline`)
+ expect(styles).not.toContain(candidate`two:underline`)
+ })
+
+ // Going to about.html will extend the candidate list to include
+ // candidates from about.html.
+ await retryAssertion(async () => {
+ let styles = await fetchStyles(port, '/root2.html')
+ expect(styles).not.toContain(candidate`one:underline`)
+ expect(styles).toContain(candidate`two:underline`)
+ })
+ },
+)