Skip to content

feat(theme): allow importing @nuxt/ui into a cascade layer#6602

Open
pupuking723 wants to merge 1 commit into
nuxt:v4from
pupuking723:fix/css-cascade-layer-import
Open

feat(theme): allow importing @nuxt/ui into a cascade layer#6602
pupuking723 wants to merge 1 commit into
nuxt:v4from
pupuking723:fix/css-cascade-layer-import

Conversation

@pupuking723

Copy link
Copy Markdown

Fixes #4503.

This moves Tailwind-only directives out of the layerable Nuxt UI stylesheet so users can import Nuxt UI into a CSS cascade layer:

@import "tailwindcss" layer(tailwindcss);
@import "@nuxt/ui" layer(tailwindcss);

Changes:

  • split generated and runtime @source directives into @nuxt/ui/sources
  • keep @variant light/dark outside the layerable @nuxt/ui stylesheet
  • load the sources stylesheet automatically from the Nuxt module
  • inject the sources stylesheet from the Vite plugin when a Vue app imports @nuxt/ui
  • add a regression test covering the cascade-layer import path

Verification:

  • pnpm exec eslint src/unplugin.ts src/module.ts src/templates.ts test/utils/css-layer.spec.ts
  • pnpm exec vitest run test/utils/css-layer.spec.ts --project vue
  • pnpm build

@github-actions github-actions Bot added the v4 #4488 label Jun 17, 2026
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR splits Nuxt UI source directives into a dedicated runtime sources stylesheet so CSS imports can be layered without nested @source errors. It adds package metadata for the new sources export and generated static file, updates template generation to separate sources output from the main UI stylesheet, wires a CSS transform plugin that injects the sources import, and adds tests covering cascade-layer compilation and charset-aware import insertion. The docs page also renames its framework ref to currentFramework.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning docs/app/pages/docs/[...slug].vue appears unrelated to the cascade-layer fix and adds scope outside #4503. Move the docs page refactor to a separate PR unless it is required for the CSS cascade-layer fix.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description matches the cascade-layer CSS changes and related verification steps.
Linked Issues check ✅ Passed The PR splits @source handling, injects the sources stylesheet, and adds tests, which aligns with #4503.
Title check ✅ Passed The title clearly matches the main change: making @nuxt/ui importable inside a CSS cascade layer.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/templates.ts (1)

386-390: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Update the watched template target for component detection.

After moving generated @source directives to ui.sources.css, the watcher still refreshes only ui.css. In dev, component-detection changes won’t regenerate sources, so Tailwind scanning can go stale until restart.

Suggested fix
-        await updateTemplates({ filter: template => template.filename === 'ui.css' })
+        await updateTemplates({
+          filter: template => template.filename === 'ui.sources.css'
+        })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/templates.ts` around lines 386 - 390, In the builder:watch hook within
the component detection block, the updateTemplates function is filtering for the
wrong template filename. Since the generated `@source` directives have been moved
to ui.sources.css, update the template filter condition from checking for ui.css
to ui.sources.css so that component detection changes in dev mode properly
regenerate the sources file and keep Tailwind scanning current.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/unplugin.ts`:
- Around line 101-107: The transform function in the unplugin.ts file currently
prepends the `@import` statement at the very beginning of the code, which violates
CSS specifications when the file starts with a `@charset` rule since `@charset` must
be the absolute first declaration. Modify the transform function to detect if
the code begins with a `@charset` rule and if so, insert the `@import` statement
after the `@charset` declaration instead of at byte 0. For files without `@charset`,
continue prepending the `@import` at the beginning as currently implemented.

In `@test/utils/css-layer.spec.ts`:
- Around line 1-9: The test file is missing the necessary imports for Vitest
global functions (describe, it, expect) which are used throughout the file
starting at line 28-42. Although vitest.config.ts enables globals: true,
TypeScript requires explicit imports for type checking. Add an import statement
from 'vitest' that imports describe, it, and expect at the beginning of the
file, following the pattern used in other test files in the repository.

---

Outside diff comments:
In `@src/templates.ts`:
- Around line 386-390: In the builder:watch hook within the component detection
block, the updateTemplates function is filtering for the wrong template
filename. Since the generated `@source` directives have been moved to
ui.sources.css, update the template filter condition from checking for ui.css to
ui.sources.css so that component detection changes in dev mode properly
regenerate the sources file and keep Tailwind scanning current.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5aafbfec-2fcb-4098-9ab6-03e1c8e37b7f

📥 Commits

Reviewing files that changed from the base of the PR and between 81745fe and 71b132e.

📒 Files selected for processing (7)
  • package.json
  • src/module.ts
  • src/runtime/index.css
  • src/runtime/sources.css
  • src/templates.ts
  • src/unplugin.ts
  • test/utils/css-layer.spec.ts
💤 Files with no reviewable changes (1)
  • src/runtime/index.css

Comment thread src/unplugin.ts Outdated
Comment thread test/utils/css-layer.spec.ts
@pkg-pr-new

pkg-pr-new Bot commented Jun 17, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/@nuxt/ui@6602

commit: 1544c42

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/utils/css.ts (1)

14-14: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Minor: CRLF files leave a stray carriage return after @charset.

code.slice(charset.length) yields \r\n@import … on CRLF stylesheets, but .replace(/^\n/, '') only strips a leading \n, so the \r (and an extra blank line) survives. Harmless for parsing but slightly messy output.

♻️ Handle CRLF too
-    return `${charset}\n${uiSourcesImport}${code.slice(charset.length).replace(/^\n/, '')}`
+    return `${charset}\n${uiSourcesImport}${code.slice(charset.length).replace(/^\r?\n/, '')}`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/css.ts` at line 14, The string assembly in css.ts leaves a stray
carriage return for CRLF stylesheets because
`code.slice(charset.length).replace(/^\n/, '')` only removes a leading LF.
Update the logic in the charset handling path to strip an initial CRLF pair as
well as LF so the result from `charset` and `uiSourcesImport` is clean on both
newline styles.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/utils/css.ts`:
- Line 14: The string assembly in css.ts leaves a stray carriage return for CRLF
stylesheets because `code.slice(charset.length).replace(/^\n/, '')` only removes
a leading LF. Update the logic in the charset handling path to strip an initial
CRLF pair as well as LF so the result from `charset` and `uiSourcesImport` is
clean on both newline styles.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1d73a57e-f50b-44e8-827f-d2525c1dbd7e

📥 Commits

Reviewing files that changed from the base of the PR and between 4cd0a59 and 724fda8.

📒 Files selected for processing (9)
  • docs/app/pages/docs/[...slug].vue
  • package.json
  • src/module.ts
  • src/runtime/index.css
  • src/runtime/sources.css
  • src/templates.ts
  • src/unplugin.ts
  • src/utils/css.ts
  • test/utils/css-layer.spec.ts
💤 Files with no reviewable changes (1)
  • src/runtime/index.css
✅ Files skipped from review due to trivial changes (1)
  • src/module.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/templates.ts
  • docs/app/pages/docs/[...slug].vue
  • src/runtime/sources.css
  • package.json

@benjamincanac benjamincanac left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @pupuking723, the @nuxt/ui/sources split is the right idea but there's a blocker on the Nuxt side.

Loading sources.css via nuxt.options.css.unshift() makes it a separate Vite module, and with @tailwindcss/vite a file is only a Tailwind root if it has @import "tailwindcss". sources.css doesn't, so its @source/@variant are never processed. I confirmed with a playgrounds/nuxt build: dark: utilities fall back to @media (prefers-color-scheme) (class toggle dead), @variant shows up as raw text, and light: utilities aren't generated.

The Vue path works only because the import gets injected into the user's main.css, which is the real root. The directives have to live in the same root as @import "tailwindcss", so either document one line in main.css outside any layer, or run the injection for Nuxt too and drop the css.unshift. Either way please add the Nuxt two root case to the test, since the current one compiles everything in a single root and stays green while Nuxt is broken.

@pupuking723 pupuking723 force-pushed the fix/css-cascade-layer-import branch from 5a02695 to 1544c42 Compare June 25, 2026 03:25
@pupuking723

pupuking723 commented Jun 25, 2026

Copy link
Copy Markdown
Author

Updated this to address the Nuxt two-root issue you pointed out.

Changes:

  • Removed the separate Nuxt nuxt.options.css.unshift(resolve('./runtime/sources.css')) root.
  • Reused the CSS sources injection plugin in the Nuxt module and registered it before @tailwindcss/vite, so @nuxt/ui/sources is injected into the user's Tailwind root CSS instead of being processed as a separate CSS module.
  • Kept the Vue/unplugin path on the same shared injection logic.
  • Added a regression that builds light:bg-primary and dark:bg-primary and asserts the Nuxt UI custom variants are present, with no raw @variant light left in the output.

Verification after rebasing onto the latest v4:

  • npx pnpm@11.8.0 test test/utils/css-layer.spec.ts
  • npx pnpm@11.8.0 exec eslint src/module.ts src/unplugin.ts src/utils/css.ts test/utils/css-layer.spec.ts
  • npx pnpm@11.8.0 build

@benjamincanac

Copy link
Copy Markdown
Member

Thanks @pupuking723! Been thinking about this one and wanted to float an idea before you go further.

What if instead of injecting @nuxt/ui/sources, we keep the imports explicit? The injection only runs through Vite so the postcss path silently loses @source and @variant, and regex rewriting the user's CSS feels a bit fragile. I also realized @theme is processed at build time like @source and @variant, so it can't sit in a layered import either, and it's still in index.css today.

The idea, fully non breaking: keep @import "@nuxt/ui" emitting everything as today, then add @nuxt/ui/sources for the directives that can't be layered (@theme + @source + @variant) and @nuxt/ui/base for the layerable CSS.

@import "tailwindcss";
@import "@nuxt/ui/sources";
@import "@nuxt/ui/base" layer(tailwindcss);

That would drop src/utils/css.ts and both plugin registrations and work everywhere, and your split and variant test mostly carry over. wdyt? Happy to push to your branch if you'd like a hand.

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

Labels

v4 #4488

Projects

None yet

Development

Successfully merging this pull request may close these issues.

@import "@nuxt/ui" layer(layer-name); not working

2 participants