What version of VS Code are you using?
1.136.0 (x64, commit 520fb30b2d3d324b4cb2342f6e88e2cd93751de1)
What version of Tailwind CSS IntelliSense are you using?
0.16.0
What version of Tailwind CSS are you using?
v4.3.0
What package manager are you using?
pnpm
What operating system are you using?
Windows 11 (10.0.26200)
Tailwind CSS Stylesheet (v4) or config file (v3)
Minimal reproduction — this file is 100% valid CSS:
/**
* This comment mentions the @variant at-rule in prose.
*/
@theme {
--color-brand: #000;
}
.a {
/* A label */
color: red;
background: blue;
}
Describe your issue
Opening the file above produces a bogus diagnostic:
{ expected (css-lcurlyexpected, source: tailwindcss) at line 7, column 8
Line 7 is .a { — perfectly valid CSS. Deleting just the @ on line 2 (@variant → variant) makes the error disappear; putting it back brings it right back.
Root cause
nf() in dist/tailwindModeServer.js rewrites Tailwind at-rules into vanilla CSS before handing the document to vscode-css-languageservice. Those rewrites are plain regexes over the whole document text, with no awareness of comments:
t = t.replace(/@screen(\s+[^{]+){/g, Id(-2))
t = t.replace(/@variants(\s+[^{]+){/g, Id())
t = t.replace(/@responsive(\s*){/g, Id())
t = t.replace(/@(utility)(\s+[^{]+){/g, ua())
t = t.replace(/@(theme)(\s+[^{]*){/g, ua())
t = t.replace(/@(custom-variant)(\s+[^;{]+);/g, ...)
t = t.replace(/@(custom-variant)(\s+[^{]+){/g, ua())
t = t.replace(/@(variant)(\s+[^{]+){/g, ua())
Two separate defects compound here.
1. The regexes match at-rule names written inside comments, and [^{]+ spans newlines.
In the repro, @variant on line 2 matches, and [^{]+ runs greedily forward until the next { — which is the one on line 4 (@theme {, already rewritten to ._____ { by the preceding pass). So lines 2–4 are consumed as a single match, including the */ on line 3 that closes the block comment.
After pre-processing the document becomes:
/**
* This comment mentions the ._______ {
--color-brand: #000;
}
.a {
/* A label */
color: red;
background: blue;
}
The /** on line 1 is now unterminated, so the CSS parser treats everything as a comment until the next */ it finds — the one in /* A label */. It resumes there, hits color: red; at the top level, parses color as a selector, and reports { expected at the :.
2. ua() does not preserve the newlines it consumes, so every subsequent diagnostic is reported at the wrong line.
function ua(t=0){return(e,n,i)=>{let r="_".repeat(n.length),s=" ".repeat(i.length+t);return`.${r}${s}{`}}
The replacement is length-preserving in characters but pads with spaces only. When the match spanned N newlines, those N lines collapse into one and all positions after it shift up by N. Id() already handles this correctly:
function Id(t=0){return(e,n)=>{let i=n.split("\n");
return i.length>1
? `@media(℘)${"\n".repeat(i.length-1)}${" ".repeat(i[i.length-1].length)}{`
: `@media(℘)${" ".repeat(n.length+t)}{`}}
…but ua() (used by @utility, @theme, @custom-variant and @variant) does not, and neither does the @custom-variant …; replacement. So @screen / @variants / @responsive corrupt comments without shifting line numbers, while the ua() group corrupts comments and shifts them.
How this looks on a real file
I hit this on a 618-line stylesheet whose header comment happened to contain the phrase `@variant dark`. That single mention swallowed 15 lines (including the comment terminator), the file was handed to the parser as 604 lines, and the error surfaced at line 110, column 13 — 14 lines away from anything related, on an ordinary --ctp-base: #eff1f5; declaration. There is no realistic way to trace that back to a word in a comment 100 lines earlier.
Suggested fix
- Skip comment ranges when applying these rewrites (tokenize, or at minimum blank out
/* … */ spans before matching and restore afterwards).
- Make every replacement newline-count-preserving, the way
Id() already is, so diagnostics land on the right line even when a rewrite does span lines. Applying this to ua() and to the @custom-variant …; branch would fix the position skew independently of the comment issue.
- Optionally, stop
[^{]+ / [^;{]+ from crossing blank lines or comment boundaries, to limit the blast radius of any future mismatch.
Related
#1565 reports the user-visible symptom (errors in @theme blocks that contain comments) and a commenter there noticed that removing the @ from comments fixes it, but that issue also bundles a separate request about color swatches and doesn't identify the pre-processing step as the cause. Filing this separately with the minimal reproduction and root cause; happy to have it closed as a duplicate if you'd rather track it there.
What version of VS Code are you using?
1.136.0 (x64, commit 520fb30b2d3d324b4cb2342f6e88e2cd93751de1)
What version of Tailwind CSS IntelliSense are you using?
0.16.0
What version of Tailwind CSS are you using?
v4.3.0
What package manager are you using?
pnpm
What operating system are you using?
Windows 11 (10.0.26200)
Tailwind CSS Stylesheet (v4) or config file (v3)
Minimal reproduction — this file is 100% valid CSS:
Describe your issue
Opening the file above produces a bogus diagnostic:
Line 7 is
.a {— perfectly valid CSS. Deleting just the@on line 2 (@variant→variant) makes the error disappear; putting it back brings it right back.Root cause
nf()indist/tailwindModeServer.jsrewrites Tailwind at-rules into vanilla CSS before handing the document tovscode-css-languageservice. Those rewrites are plain regexes over the whole document text, with no awareness of comments:Two separate defects compound here.
1. The regexes match at-rule names written inside comments, and
[^{]+spans newlines.In the repro,
@varianton line 2 matches, and[^{]+runs greedily forward until the next{— which is the one on line 4 (@theme {, already rewritten to._____ {by the preceding pass). So lines 2–4 are consumed as a single match, including the*/on line 3 that closes the block comment.After pre-processing the document becomes:
The
/**on line 1 is now unterminated, so the CSS parser treats everything as a comment until the next*/it finds — the one in/* A label */. It resumes there, hitscolor: red;at the top level, parsescoloras a selector, and reports{ expectedat the:.2.
ua()does not preserve the newlines it consumes, so every subsequent diagnostic is reported at the wrong line.The replacement is length-preserving in characters but pads with spaces only. When the match spanned N newlines, those N lines collapse into one and all positions after it shift up by N.
Id()already handles this correctly:…but
ua()(used by@utility,@theme,@custom-variantand@variant) does not, and neither does the@custom-variant …;replacement. So@screen/@variants/@responsivecorrupt comments without shifting line numbers, while theua()group corrupts comments and shifts them.How this looks on a real file
I hit this on a 618-line stylesheet whose header comment happened to contain the phrase
`@variant dark`. That single mention swallowed 15 lines (including the comment terminator), the file was handed to the parser as 604 lines, and the error surfaced at line 110, column 13 — 14 lines away from anything related, on an ordinary--ctp-base: #eff1f5;declaration. There is no realistic way to trace that back to a word in a comment 100 lines earlier.Suggested fix
/* … */spans before matching and restore afterwards).Id()already is, so diagnostics land on the right line even when a rewrite does span lines. Applying this toua()and to the@custom-variant …;branch would fix the position skew independently of the comment issue.[^{]+/[^;{]+from crossing blank lines or comment boundaries, to limit the blast radius of any future mismatch.Related
#1565 reports the user-visible symptom (errors in
@themeblocks that contain comments) and a commenter there noticed that removing the@from comments fixes it, but that issue also bundles a separate request about color swatches and doesn't identify the pre-processing step as the cause. Filing this separately with the minimal reproduction and root cause; happy to have it closed as a duplicate if you'd rather track it there.