This will bring the development version and production version closer
together. It also means that the development version works in older
supported browsers like Chrome v112 where the space is required (even
though the spec allows both)
There are no changes in tests just becaues we already run each test
through Lightning CSS which normalizes these values:
Input:
```css
var(--tw-blur,)
var(--tw-blur, )
```
Lightning CSS output:
```css
var(--tw-blur, )
var(--tw-blur, )
```
Continuation of #20344, original commits of that PR are present in this
PR as well.
Before we talk about the issues, there is some jargon I'll be using that
might be unfamiliar:
```css
@scope (.from) to (.to) {
/* ^^^^^^^^^^^^^^^^ This is the prelude */
/* ^^^^^^^ This is the `scope-start` */
/* ^^^^^ This is the `scope-end` */
}
/* The `scope-start` is optional, if you only want `scope-end`: */
@scope to (.to) {
}
/* The `scope-end` is optional, if you only want `scope-start`: */
@scope (.from) {
}
```
This PR contains multiple fixes, but they are all related to the
`@scope` at-rule. The `@scope` at-rule is a beast, it's an at-rule where
we do have multiple selectors in the prelude, which in turn can contain
`&` rules. If you use `&` inside of an `@scope` rule, its meaning
changes compared to what `&` typically means and it is even different
whether it exists in `scope-start` or `scope-end`.
Let's start with the original issue, if you define a custom variant
like:
```css
@custom-variant blue {
@scope ([data-theme='blue']) to ([data-theme]) {
@slot;
}
}
```
Then using `blue:bg-blue-500` used to generate:
```css
.blue\:bg-blue-500 {
@scope ([data-theme='blue']) to ([data-theme]) {
background-color: var(--color-blue-500);
}
}
```
This is because internally we start with the AST node representing
`bg-blue-500`, then we wrap all the `variant` related nodes around it
(`blue`), and then wrap everything in the final rule with a selector
representing the class (`.blue\:bg-blue-500`).
This is incorrect because you want that sandwich effect where any
`.blue\:bg-blue-500` class between an element with the
`[data-theme="blue"]` attribute and another nested `[data-theme]`
attribute.
The other [PR](#20344) solved this by simply making sure that the
`@scope` rule gets hoisted to the top. For this particular example that
was enough.
However, that would result in a ton more issues. This hoisting or
swapping with the rule is only "safe" in a `custom-variant`.
So similar to the other PR, this PR wraps this properly:
```css
@scope ([data-theme='blue']) to ([data-theme]) {
.blue\:bg-blue-500 {
background-color: var(--color-blue-500);
}
}
```
The tricky part is that the nesting pass runs on _all_ CSS, custom
variants _and_ your own CSS. Hoisting an `@scope` rule out of the rule
it is nested in changes its meaning.
Take a look at this example:
```css
.parent {
@scope (.from) to (.to) {
* {
border: 1px solid black;
}
}
}
```
This says that you need a structure roughly like this:
```html
<div class="parent">
<div class="from">
<div>This element will get a border</div>
<div class="to">
<div>This element won't get a border, it's beyond the scope</div>
</div>
</div>
</div>
```
If we had switched it:
```css
@scope (.from) to (.to) {
.parent {
* {
border: 1px solid black;
}
}
}
```
Then this requires that an element with a class of `.parent` lives
between the
`.from` scope-start and `.to` scope-end:
```html
<div class="from">
<div class="parent">
<div>This element will get a border</div>
<div class="to">
<div>This element won't get a border, it's beyond the scope</div>
</div>
</div>
</div>
```
Subtle, but it's definitely wrong.
Instead, we will apply our flattening rules, such that the `@scope` gets
hoisted, but we have to bring that `.parent` selector requirement inside
of the `@scope` prelude, more specifically inside the `scope-start`:
```css
@scope (.parent .from) to (.to) {
* {
border: 1px solid black;
}
}
```
We _could_ have left this as-is, but there is a Lightning CSS bug where
the original CSS:
```css
.parent {
@scope (.from) to (.to) {
* {
border: 1px solid black;
}
}
}
```
gets turned into:
```css
@scope (.from) to (.to) {
:scope * {
border: 1px solid #000;
}
}
```
Notice how the `.parent` class is not found at all? (I will open some
issues on the Lightning CSS repo (and PRs to fix this). But it's a good
fix to have in Tailwind CSS as well, just because a lot of tooling is
already using Lightning CSS on top of Tailwind CSS, so we need an entire
chain to be updated for this to work.)
Side note: the version we generate is untouched by Lightning CSS.
## Resolving `&` in the prelude
`&` resolves differently depending on which scope selector you're
looking at
([spec](https://drafts.csswg.org/css-nesting-1/#nesting-at-scope)):
- In the `<scope-start>` selector, `&` refers to the elements matched by
the nearest ancestor style rule (the utility, for variants).
- In the `<scope-end>` selector, `&` refers to the scoping root and
behaves like `:where(:scope)`. `:scope` might work, but that has a
specificity of `0,1,0` instead of `0,0,0`.
So this input:
```css
.parent {
@scope (& > .scope) to (& .limit) {
.content {
color: red;
}
}
}
```
compiles to:
```css
@scope (.parent > .scope) to (:where(:scope) .limit) {
.content {
color: red;
}
}
```
The substitution uses the exact same logic as `&` in nested style rule
selectors: `:is(…)` semantics by default, dropping the `:is(…)` whenever
the substitution is provably equivalent.
So a complex parent compiles to `@scope (.a .b > .from)` rather than
`@scope (:is(.a .b) > .from)`, while `.card&` inside a `main` rule keeps
it (`@scope (.card:is(main))`) because substituting the type selector
as-is would produce invalid CSS.
For user CSS, every selector in a `<scope-start>` selector list is
relative to the parent rule, whether it uses `&` or not, just like
nested style rule selectors:
```css
.parent {
@scope (.a, & > .b) {
.inside {
color: red;
}
}
}
```
compiles to:
```css
@scope (.parent .a, .parent > .b) {
.inside {
color: red;
}
}
```
## Controlling the composition with `&`
Because `&` in the `<scope-start>` selector refers to the utility,
variant authors can control where the utility ends up:
```css
/* Default: the utility applies to elements inside the scope */
@custom-variant in-blue {
@scope ([data-theme='blue']) to ([data-theme]) {
@slot;
}
}
/* Trailing `&`: the utility itself becomes the scoping root */
@custom-variant blue-self {
@scope ([data-theme='blue'] &) to ([data-theme]) {
@slot;
}
}
/* Leading `&`: the scoping roots are found inside the utility */
@custom-variant scoped-panel {
@scope (& .panel) {
@slot;
}
}
```
generates:
```css
@scope ([data-theme='blue']) to ([data-theme]) {
.in-blue\:flex {
display: flex;
}
}
@scope ([data-theme='blue'] .blue-self\:flex) to ([data-theme]) {
:where(:scope) {
display: flex;
}
}
@scope (.scoped-panel\:flex .panel) {
:where(:scope) {
display: flex;
}
}
```
Note that when the utility becomes the scoping root, the `<scope-end>`
limit no longer affects the utility itself (a scoping root is never
beyond its own limits), and the declarations apply to the root with zero
specificity. The default composition is usually what you want to get the
sandwich effect, but now you _can_ get the other behavior if you want.
## Bare declarations
There is another small Lightning CSS bug that we want to fix. If you
have the following CSS:
```css
@scope (.from) to (.to) {
color: red;
}
```
Then Lightning CSS crashes with an unexpected input. The [spec
says](https://drafts.csswg.org/css-cascade-6/#scoped-declarations) that:
> Declarations may be used directly with the body of a `@scope` rule.
Contiguous runs of declarations are wrapped in nested declarations
rules, which match the scoping root with zero specificity.
>
> ```css
> @scope (.foo) {
> border: 1px solid black;
> }
> ```
> is equivalent to:
> ```css
> scope (.foo) {
> :where(:scope) {
> border: 1px solid black;
> }
> }
> ```
So this is what we will do as well:
```css
@scope (.from) to (.to) {
:where(:scope) {
color: red;
}
}
```
---
One more small detail about the implementation: we do have to track
which CSS is coming from the custom variant, and what is user defined
CSS. To do this, we insert some internal `context` nodes with a `{
source: 'user' }` or `{ source: 'variant' }` so we know when we can just
swap this `@scope` around, or if we just have to handle the nesting and
`&` substitutions.
Fixes: #18961Closes: #20344
## Test plan
1. Added loads of new tests related to `@scope` in user-land
1. Added loads of new tests related to `@scope` in custom variants
- When using the `@custom-variant` shorthand syntax
- When using the `@custom-variant` syntax with `@slot`
- When using arbitrary variants like `[@scope_(.from)_to_(.to)]:flex`
- When using `addVariant()` and `matchVariant()` APIs
1. Added tests for the hoisting logic, prelude resolution for `&`
selectors
1. Added browser based UI tests to make sure that the non-flattened
version and flattened version behave the same.
---
Sorry about all the sandwiches, I'm hungry myself.
---------
Co-authored-by: uditDewan <udit.dewan21@gmail.com>
This PR updates the integration tests that started failing because of a
recent PostCSS update.
PostCSS v8.5.24 started re-emitting the BOM character in the output,
see: https://github.com/postcss/postcss/releases/tag/8.5.24 therefore
the tests started failing.
This PR just re-adds that back.
It might not be clear from the diff on GitHub, but this is the change:
<img width="878" height="84" alt="image"
src="https://github.com/user-attachments/assets/5e2fbb6e-2ee4-46c5-9a2c-9c3a1a806bb0"
/>
## Test plan
1. All tests pass again [ci-all]
This PR fixes an issue where changes to a symlinked file wouldn't result
in hot-reload when using `@tailwincdss/vite`. This issue also exists in
the other packages such as `@tailwincdss/postcss`,
`@tailwincdss/webpack` and `@tailwincdss/cli`.
The issue is that we watch the symlinked file, but not the "real" file
for changes. If the source of the symlinked file lives in another folder
that is not covered by auto-source detection or by any of the `@source`
directives, then changes to that file won't trigger a change.
To solve this, if a file is symlinked or lives in a symlinked folder,
then we will make sure that the `scanner.files` contains the real path /
canonicalized path to the real file as well just so we can detect
changes in that file.
Fixes: #20346Closes: #20347
## Test plan
1. Added a regression test for `@tailwindcss/vite`
2. Added tests in the scanner code itself
3. Manually tested on the reproduction:
| | Initial state | After change |
| ---: | --- | --- |
| **Before** | <img width="3200" height="1800"
alt="file-f8616573eec3150ae484fd279204c6d7"
src="https://github.com/user-attachments/assets/2734ecc3-b5b6-420e-820d-8a4a8fcdd7f5"
/> | <img width="3200" height="1800"
alt="file-0e93fd3c2c9694c844b098616a3208d2"
src="https://github.com/user-attachments/assets/fd86859b-420b-476b-80f6-e94d02e8b07b"
/> |
| **After** | <img width="3200" height="1800"
alt="file-f8616573eec3150ae484fd279204c6d7"
src="https://github.com/user-attachments/assets/2734ecc3-b5b6-420e-820d-8a4a8fcdd7f5"
/> | <img width="3200" height="1800"
alt="file-eb7b37430ea1363dddeac7226007afe4"
src="https://github.com/user-attachments/assets/0a95b1a4-a357-491d-b57f-78460c0fc9db"
/> |
[ci-all]
---------
Co-authored-by: Nic <162764842+Nic-Polumeyv@users.noreply.github.com>
The last CI run on `main` was on July 16, and the last `globby` release
(16.2.2) was released on July 15 (~21 hours earlier). Due to the default
`minimumReleaseAge` in pnpm of 24 hours, it meant that we were receiving
the previous globby version.
In the new version, they slightly changed some of the `.gitignore`
parsing, but in one of our upgrade tests we wrote an invalid
`.gitignore` file (which had indented lines).
This PR fixes that by using the `txt` tagged template literal, which
behind the scenes uses `dedent` which in turn makes sure that we ship a
proper `.gitignore` file.
## Test plan
All tests should pass [ci-all]
This PR fixes an issue where editing a scanned file that Vite (or one of
its plugins) can process as a module, but that isn't currently loaded,
caused `@tailwindcss/vite` to force a full page reload, throwing away
all client state.
The `hotUpdate` hook has a fallback that sends a `full-reload` for files
that Tailwind scans but that Vite knows nothing about (e.g. `.php` or
`.blade.php` templates rendered by a backend). Without it, edits to
those files wouldn't refresh the page at all. To detect those files we
check whether every module for the changed file is an `asset` and/or has
no id, because the scanner's `addWatchFile` calls create exactly such
placeholder nodes for every scanned file.
The problem is that a source file that Vite _can_ process, but that
isn't loaded yet, looks exactly the same. The realistic way to get into
that state is code splitting: with route-level splitting (e.g.
`React.lazy`, TanStack Router's `autoCodeSplitting`, lazy routes in
`vue-router`), every component behind an un-visited split boundary only
exists as a scan placeholder in the module graph. Editing any of them
reloaded the whole app. The same happens for component stylesheets that
a framework plugin compiles into the component (e.g. Angular via
Analog), which never show up as their own module.
A full reload is never useful for these files: if the file is loaded,
Vite's own HMR handles it, and if it isn't loaded, reloading the page
won't load it either. Any new candidates still apply through the regular
`css-update` flow because the file is registered via `addWatchFile`.
So instead, we now skip the fallback when the changed file is handled by
Vite's module pipeline:
- The file exists as a real module in another environment (e.g. an
SSR-only module). This check already existed and is folded into the same
code path.
- The file is part of the JS/TS or CSS families, which Vite transforms
natively.
- For any other file type (e.g. `.vue`, `.svelte`, or `.md` with an SSG
plugin), a file with the same extension exists as a real module in some
environment's module graph, then a plugin does handle this file type and
the changed file just isn't loaded (yet).
External templates like `.php` files still trigger a full reload exactly
like before.
Fixes: #20320Fixes: #19903Closes: #20323
## Test plan
1. Added integration tests to ensure extensions handled by default rely
on HMR
2. Added integration tests to make sure that unknown extensions that
have been handled already will also use HMR
3. Manually tested that changing a `.php` file still triggers a
`full-reload`
4. Manually tested the reproduction where local client state isn't
thrown away
<img width="594" height="100"
alt="file-14a86a90a1e4b810c2b80338ea688572"
src="https://github.com/user-attachments/assets/a4507502-4a5d-43ee-93b9-14c793a25891"
/>
<img width="1122" height="1376"
alt="file-a5121da2ad77b95fdd1703560ef1ff41"
src="https://github.com/user-attachments/assets/cc5c67b0-b5ad-481f-8823-a3266d75357d"
/>
This PR fixes an issue where an `@source` pointing to a file in a nested
folder was not scanned when a later `@source` pointed to a file in a
parent folder.
E.g.:
```css
@source "./nested/index.html";
@source "./index.html";
```
When using `@source` pointing to a specific file, then we want to make
sure that we ignore _other_ files since they are not listed explicitly.
To ensure that these patterns don't read other files, we inject a `*`
ignore pattern before it. You can think of the above being expanded to:
```rs
Ignored { base: "/project/src/nested", pattern: "*" }
Pattern { base: "/project/src/nested", pattern: "/index.html" }
Ignored { base: "/project/src", pattern: "*" }
Pattern { base: "/project/src", pattern: "/index.html" }
```
The problem with this is that the `Ignored { base: "/project/src",
pattern: "*" }` pattern results in ignoring the `nested` folder as well.
This means that we never even walk into the `nested` folder, so the
earlier `@source "./nested/index.html"` never matches anything.
We could switch the order in user land, but that's going to be hard to
maintain (and order matters for undoing/redoing earlier rules, so we
can't re-order internally either). Instead, we can scope the ignore
pattern to the _current_ path only, and not deeply nested. In other
words, the pattern should become:
```diff
- *
+ /*
```
It's a very subtle difference, but the pattern from above will now
become:
```diff
Ignored { base: "/project/src/nested", pattern: "*" }
Pattern { base: "/project/src/nested", pattern: "/index.html" }
- Ignored { base: "/project/src", pattern: "*" }
+ Ignored { base: "/project/src", pattern: "/*" }
Pattern { base: "/project/src", pattern: "/index.html" }
```
We already do this when an unrestricted root (e.g. `@source "./nested"`)
lives inside the base of a restricted pattern. This works because every
source base is also its own walk root, and a `/*` pattern only matches
direct children so it can't ignore anything when walking from the nested
root itself.
This PR extends that same check to restricted pattern bases: if another
`@source` pattern has its base nested inside the current base, we emit
`/*` instead of `*`.
Note that we only relax the pattern to `/*` when such a nested root
actually exists. Sibling folders that no `@source` points into (e.g. an
`ignore-me` folder next to `nested`) are still direct children, so they
still match `/*` and are never walked.
Fixes: #20333
## Test plan
1. Added a regression test with the reproduction setup
2. Added a similar test with another sibling folder that should still be
ignored
3. Tested it against the actual reproduction:
Before:
<img width="797" height="463" alt="image"
src="https://github.com/user-attachments/assets/893bb872-f5cf-4c5a-bfb5-2dc1043b5a39"
/>
After:
<img width="794" height="460" alt="image"
src="https://github.com/user-attachments/assets/a59aae45-68ad-4fe3-b21f-b568f535d24f"
/>
Notice that the `text-green-500` now appears as expected.
Fixes#20328.
## What changed
When `@tailwindcss/upgrade` is invoked from a subpackage of a workspace
(e.g. `pnpm --filter …` or `cd packages/foo && pnpm exec upgrade`), it
traverses `../../node_modules/…` and applies its v3 → v4 migration
transform (`@tailwind utilities;` → `@import 'tailwindcss/utilities'
layer(utilities);`) to files inside the installed `tailwindcss` package
itself — a self-referential import that later detonates as an infinite
CSS-resolution loop and reads like a "cache corruption" bug (matches the
report in #19726 / #20328).
Root cause: `globby`'s `isGitIgnored` only walks `.gitignore` files at
or below its `cwd`. When invoked from a subpackage, the workspace-root
`.gitignore` (which almost always lists `node_modules`) is never
consulted, and `../../node_modules/…` paths come back as **not**
ignored.
The fix anchors `isGitIgnored` at the git repository root instead of the
subpackage:
- New helper: `gitRoot(cwd)` in `src/utils/git.ts` — thin wrapper over
`git rev-parse --show-toplevel`.
- `analyze(stylesheets, { base })` in `src/codemods/css/analyze.ts` now
calls `isGitIgnored({ cwd: gitRoot(base) ?? base })`. Falls back to the
previous behavior when not inside a git repository or when `git` is
unavailable.
## Test
Added an integration test that reproduces the exact scenario: a pnpm
workspace with a subpackage that imports `tailwindcss/utilities.css`,
upgrade run from the subpackage, then asserts
`packages/css/node_modules/tailwindcss/utilities.css` still holds the
pristine `@tailwind utilities;` directive.
Verified the test fails on `main` and passes with this change.
The 415 existing upgrade unit tests still pass. Two npm-related snapshot
failures in `upgrade-errors.test.ts` (`half-upgraded v3 project to v4
(bun|npm)`) pre-exist this change — they're about newer npm's `npm
notice run` output that isn't in the snapshots, unrelated to what this
PR touches.
---
Edit by @RobinMalfait: going to run on all [ci-all] so we can make sure
it works on Windows as well.
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This PR fixes an issue where some characters are incorrectly rendered on
Windows with the Japanese locale.
This is arguably a bug in the font that's loaded by Windows when it
encounters `system-ui`. But waiting for fixes there might ... take a
while.
Another option is to not change the defaults in Tailwind CSS and instead
let the users that support different locales implement a fallback by
overriding the `--font-sans` variable.
The biggest reason for me to _not_ change it in Tailwind CSS is that it
requires us to know what the (proper) fallback fonts need to be on a per
OS basis.
But the main reason why I did want to make the change is that MDN says
this about the `system-ui` font:
> Glyphs are taken from the default user interface font on a given
platform. Because typographic traditions vary widely across the world,
this generic is provided for typefaces that don't map cleanly into the
other generics.
>
> **Note:** As the name implies, `system-ui` is intended to make UI
elements look like native apps, and not for typesetting large paragraphs
of text. It may cause the displayed typeface to be undesirable for some
users—for example, the default Windows CJK font may render Latin scripts
poorly, and the `lang` attribute may not affect the displayed font. Some
operating systems do not allow customizing `system-ui`, while browsers
generally allow customizing the `sans-serif` font family. For large
paragraphs, use `sans-serif` or some other non-UI font family instead.
>
> —
https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/font-family#system-ui
There are PRs in other big projects that made this kind of change as
well. E.g.:
- https://github.com/withastro/starlight/pull/3729
- https://github.com/vuejs/vitepress/pull/4988
The reasoning for getting rid of `ui-sans-serif` is twofold:
1. Because the starlight PR seems very well tested, and they got rid of
it
2. In the event that the browser decided to load the broken font when it
encounters `ui-sans-serif`, then we will run into the same issue again.
Fixes: #19767Fixes: #19768
## Test plan
1. `system-ui` is not used anymore, so the bug doesn't happen
3. Everything still looks the same for the places I checked, but it's
hard to know if this created _other_ issues on other OS + Locale
combinations...
This PR ensure that we lazy load the `@parcel/watcher` in the
`@tailwindcss/cli`. This means that we don't have to load it at all when
using a normal build or when using `--watch --poll` combination.
The bigger reason is that on some platforms `@parcel/watcher` might not
work, and therefore the build will fail even if you don't use `--watch`
at all.
This PR fixes that by lazy loading it. Then, if we can't load it when
using `--watch`, a useful workaround is shown by using `--watch --poll`
instead.
This PR also improves showing errors such that the `error.cause`
property can be rendered as well.
Maybe in the future we can make use of deferred imports
(https://github.com/tc39/proposal-defer-import-eval)
Fixes: #20322
## Test plan
1. All tests still pass [ci-all]
2. Fabricated a fake error locally to prove that we can still use a
normal build and `--watch --poll` as a workaround
<img width="1122" height="1376"
alt="file-2f91d99cfb9eb0e24a196c2dd358c1f3"
src="https://github.com/user-attachments/assets/e37658ee-0827-422e-bd50-8ea5a85e1b7c"
/>
This PR fixes a type issue when using `--spacing(0)`. This construction
doesn't really make sense, but if you use it, it produces the value `0`
instead of `calc(var(--spacing) * 0)` which is fine if you use it in a
spot where a `<length>` data type can be used, then the `0` is
interpreted as a `<length>`.
E.g.: `padding: 0` and `padding: 0px` are equivalent.
However, if you use it in a CSS variable, then the `0` will turn in a
`<number>` if you don't have an `@property` definition for that CSS
variable.
This on its own isn't the issue, but if you later use it as part of a
`calc(…)` then the `<number>` instead of `<length>` type is being used.
As seen in #20315.
We could remove the optimization, and use `calc(var(--spacing) * 0)`
again, but this is a bit silly since it only makes your CSS file larger.
We could try to be smart, and only do it _if_ we're assigning to a CSS
variable (which #20317 is doing). But if your CSS variable _does_ use a
`<length>` then it's a non-issue. We would run into the same issue if
you use `width: calc(100% --spacing(0))`. This value is a bit silly
anyway, but it would originally resolve to `calc(100% +
calc(var(--spacing) * 0))`, the optimization of `calc(100% - 0)` would
make it invalid, so the fix in #20317 would not be enough.
Instead I opted for an inbetween solution, by always using `0px`. In
most cases we can use `0`, but in the places we can't the `0px` would at
least ensure that we are dealing with `<length>` data types.
This way we don't have to try to be smart to analyze where we use the
value, and `0px` is still better than the long `calc(var(--spacing) *
0)` value.
Fixes: #20315Closes: #20317
## Test plan
1. Manually tested that now `--spacing(0)` does produce `0px` which has
a length type
This PR introduces a new feature where we will be handling the CSS
nesting ourselves.
We currently still rely on Lightning CSS in most places. But there are
situations where we don't use Lightning CSS out of the box:
1. During development, typically optimization/minification isn't setup
2. In places where it isn't as easy to run Lightning CSS such as in
`@tailwindcss/browser` or in Tailwind Play.
We handle CSS nesting in a single pass over the AST by tracking some
information as we go. It's not the most complex code, but there are some
tricky parts to make this happen in an efficient way, especially for the
few additional optimizations we handle.
While going over the AST, we will only emit CSS the moment we see
declarations or comments. This also means that this has a fun side
effect of removing CSS that ends up with empty nodes automatically.
(Caveat: there are exceptions for body-less rules such as `@layer foo;`
or `@charset "UTF-8";)
This also allowed us to do some cleanup in `optimizeAst` that tried to
do this as well, but now this will be handled by the code that handles
nesting automatically. Which is preferred because the version in
`optimizeAst` mutated the AST.
This also contains some optimizations where we merge adjacent at-rules
(with the same name / params), and adjacent rules with the same
selector, and get rid of declarations that are duplicated in a node.
(Caveat: there are exceptions, in case of `@font-family { … }` where we
don't want to merge them)
~~To ensure that this implementation is correct, I also added an oracle
implementation in the tests. This implementation does multiple passes
over the AST, because it does each step one by one, with minimal code.
Each step contains comments with examples to see what's happening in
that step. We then test the optimized version against this.~~ Once the
implementation was in place, and all the tests were passing, then I
deleted the oracle implementation. That way we don't have to keep it in
sync all the time.
While handling the nesting, we have to make sure that `&` exists and if
we replace it with a parent selector that we do use `:is(…)` semantics.
This means that:
```css
.foo {
&:hover {
color: red;
}
}
```
Becomes:
```css
:is(.foo):hover {
color: red;
}
```
We then also make sure that we optimize the selector by removing the
unnecessary `:is(…)` wrappers, but only if they were introduced by the
nesting logic. If _you_ wrote `:is(…)` in your CSS, we won't touch it.
If you look at the commits, the first thing we did is remove the
optimization step from Lightning CSS in the tests. Then we enabled our
CSS nesting handling code. This allows us to see the effect of the
changes we are making. At the end, we re-enabled Lightning CSS.
For now, this PR will be a step that happens before Lightning CSS is
executed, while still using Lightning CSS. But now this step will also
always happen in places where we don't use Lightning CSS at all.
This should not result in any breaking changes. It could result in
changed CSS output in environments where Lightning CSS isn't used. In
environments where it is being used, then there could be some
differences related to some selectors but they should result in the same
behavior with the same specificity.
While testing things, I noticed that there are some missed opportunities
for performance related to how we extract variables from declaration
values. I want to tackle `optimizeAst` in future PRs to make it simpler,
more performant, and maybe even merge it with the CSS nesting handling.
As part of testing this, I tested it against the tailwindcss.com
codebase which contains a lot of CSS (807.67 KB, 18 174 AST nodes)
because almost every utility is being used in examples.
The oracle implementation is rather slow:
```
[131.59ms] ↳ oracle (step by step)
[129.23ms] ↳ handleNesting(…)
[ 2.32ms] ↳ toCss(…)
```
But the final code is much faster (`<15ms`):
```
[ 10.11ms] ↳ hand written (single pass)
[ 8.40ms] ↳ handleNesting(…)
[ 1.67ms] ↳ toCss(…)
```
In contrast, Lightning CSS takes: `[ 37.48ms] Optimized by Lightning
CSS`
One interesting thing to notice is that in big projects, this could add
`10ms` to the build, but Lightning CSS would then take less time to
process, which results in a no-op with better output.
One thing to keep in mind here is that Lightning CSS does more things,
such as normalizing values, handling vendor prefixes, CSS nesting, etc.
<details>
<summary>Some notes on how the algorithm works:</summary>
### The basic idea
When you have CSS that looks this:
```css
.foo {
.bar {
color: red;
}
}
```
Then the AST looks like this:
```
[
{
kind: 'rule',
selector: '.foo',
nodes: [
{
kind: 'rule',
selector: '.bar',
nodes: [
{
kind: 'declaration',
property: 'color',
value: 'red',
important: false
}
]
}
]
}
]
```
When we walk this tree, and we encounter a `rule`, then we will track
the selector on a stack. When we are done walking over the rule, then we
will pop the selector from the stack. This means that the top-most
selector on the stack will always be the parent selector.
```ts
let selectorStack = []
walk(ast, {
enter(node) {
selectorStack.push(node.selector)
},
exit(node) {
selectorStack.pop()
},
})
```
The moment we encounter a `rule`, and if a previous rule was seen, then
we push the `selector` of the rule onto the stack, but in a way that the
`&` is already replaced by the selector. This way, a sibling rule will
also get the already-prepared parent selector.
The simple version looks like this:
```ts
walk(ast, {
enter(node) {
// In the real code we properly handle `&` replacement, and make sure that
// parent selector is prepended if there is no `&` used in the selector of the
// node.
let selector =
selectorStack.length > 0
// At this point, we don't optimize anything related to the selector yet
? node.selector.replaceAll('&', `:is(${selectorStack.at(-1)})`)
: node.selector
selectorStack.push(selector)
},
exit(node) {
selectorStack.pop()
},
})
```
So far we aren't doing much yet, but the interesting part is when we
encounter a `declaration` (or a `comment`). The moment we see any of
those, then will we emit a node with the information from the
`selectorStack`.
We then also track the last node's `nodes` we created such that we can
push more declarations into it as a shortcut.
```ts
let result: AstNode[] = []
let nodes: AstNode[] | null = null
walk(ast, {
enter(node) {
if (node.kind === 'declaration') {
// `nodes` is available, nothing special to do
if (nodes) {
nodes.push(node)
return
}
// Track new nodes
let nodes = [node]
// Create a new node with a reference to `nodes` for future declarations
let newNode = rule(selectorStack.at(-1), nodes)
result.push(newNode)
}
},
})
```
The last important part is that whenever we see a new `rule`, then we
have to reset that `nodes` tracking variable such that we can create a
fresh node the next time we see a declaration.
For the `at-rules`, something similar happens but they are tracked in a
similar but separate stack. The idea there is that we can then wrap
those `at-rules` around the `newNode` we create. That way the at-rules
naturally float to the top.
I can keep going here, but I think if you're interested in this, then
you could go over the commits in this PR, or you can look at the
`ast.ts` implementation directly to see what's going on.
</details>
## Test plan
1. Existing tests should pass
2. New tests have been added to test the flattening of nested CSS
## Summary
`@tailwindcss/postcss` chooses between an incremental and a full rebuild
by comparing the mtimes of the entry file and its resolved
`@import`/`@config`/`@plugin` graph. It never looks at the input CSS
itself, so when that CSS is produced by an upstream tool (e.g. Sass) and
passed to the plugin via `process()`, it can change while the `from`
file's mtime stays the same. The plugin then re-emits its previously
cached output and silently drops the change.
This stores the input CSS per cache entry and takes the existing full
rebuild path when it differs from the previous compile, mirroring the
fix the CLI watcher already has for changed input files.
Fixes#20307
## Test plan
- Added a regression test in
`packages/@tailwindcss-postcss/src/index.test.ts` that compiles two
different inputs for the same on-disk `from` file (unchanged mtime) and
asserts the second compile reflects the new CSS.
- Confirmed it fails on `main` (the second compile returns the stale
first output) and passes with the fix.
- Ran the `@tailwindcss/postcss` package tests (all green) and checked
formatting with Prettier.
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This PR fixes a bug in the selector parser where an attribute selector
followed by a type selector inside a compound selector resulted in the
wrong result.
Given you have this CSS:
```css
[data-foo]div {}
```
Then parsing it before this PR, would result in:
```ts
{
kind: 'compound',
nodes: [
{ kind: 'selector', value: '[data-foo]div' },
],
},
```
But with this PR, it's properly split:
```ts
{
kind: 'compound',
nodes: [
{ kind: 'selector', value: '[data-foo]' },
{ kind: 'selector', value: 'div' },
],
},
```
I also tweaked some of the comments in the parser that are unrelated,
but I was there already.
## Test plan
1. Added a regression test
2. All other tests should pass
## Summary
`shadow-*`, `text-shadow-*`, `drop-shadow-*`, and `inset-shadow-*`
accept a bare (non-arbitrary) opacity modifier like `/50`, but the
named-size branch of all four utilities validates it with
`isPositiveInteger(candidate.modifier.value)` instead of
`isValidOpacityValue(candidate.modifier.value)` — the helper every other
opacity/alpha modifier in the codebase uses (`asColor`, used by `bg-*`,
`text-*`, `border-*`, `ring-*`, `fill-*`, `stroke-*`, `decoration-*`,
`accent-*`, `caret-*`, `outline-*`, `placeholder-*`, `divide-*`).
This means a fractional modifier like `/12.5` is silently ignored for a
*named size* (`shadow-sm/12.5` behaves exactly like `shadow-sm`,
dropping the modifier), while the exact same `/12.5` modifier works
correctly on the *color* variant of the same utility
(`shadow-red-500/12.5` → `color-mix(in oklab, var(--color-red-500)
12.5%, transparent)`), since that path already goes through `asColor`.
`drop-shadow-*` is worse: its named-size branch has an extra guard (`if
(candidate.modifier && !alpha) return`) that bails out of the *entire*
utility when a modifier is present but couldn't be resolved to an alpha
— so `drop-shadow-sm/12.5` produces no CSS at all.
This isn't a case of fractional percentages being unsupported by design
— the CHANGELOG entry that introduced `shadow-*/<alpha>` explicitly
describes it as controlling shadow **opacity**, and the color branch of
these same utilities already supports fractional values
(`shadow-red-500/2.25`, `/2.5`, `/2.75` are covered by existing tests).
The named-size branch just never got the same treatment.
**Repro** (verified with `pnpm --filter tailwindcss exec vitest run`):
- `shadow-red-500/12.5` → `color-mix(in oklab, var(--color-red-500)
12.5%, transparent)` (correct)
- `shadow-sm/12.5` → identical output to plain `shadow-sm` (modifier
silently dropped)
- `drop-shadow-sm/12.5` → **no CSS generated at all**
- `shadow-sm/50` (integer, control) → works correctly
## Fix
Replaced `isPositiveInteger(candidate.modifier.value)` with
`isValidOpacityValue(candidate.modifier.value)` in the four affected
utility definitions in `packages/tailwindcss/src/utilities.ts`
(`shadow`, `text-shadow`, `drop-shadow`, `inset-shadow`). No other
changes were needed — once `alpha` resolves correctly, `drop-shadow`'s
existing `if (candidate.modifier && !alpha) return` guard naturally
stops bailing out, since `alpha` is no longer `undefined` for valid
fractional modifiers.
## Test plan
- Added a regression test in
`packages/tailwindcss/src/utilities.test.ts` covering `shadow-sm/12.5`,
`text-shadow-sm/12.5`, `drop-shadow-sm/12.5`, and
`inset-shadow-sm/12.5`.
- Verified via `pnpm --filter tailwindcss exec vitest run` that this
test fails (modifier dropped / empty output) with the fix reverted, and
passes with it applied.
- Ran the full `tailwindcss` package test suite (`pnpm --filter
tailwindcss exec vitest run`) — 4685 tests passing, no regressions.
- Verified formatting on the changed files with `npx prettier --check`.
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
## Summary
When a CSS theme key defined via `@theme` (or a JS config's `theme`
object) shares a dash-separated prefix with a sibling key — e.g.
`--color-foo` and `--color-foo-bar` — calling `theme('colors.foo')` from
inside a JS plugin (`addUtilities`, `addComponents`, etc.) does not
resolve to the `foo` value. Instead it returns an internal
disambiguation object shaped like `{ DEFAULT: 'red', bar: 'blue',
__CSS_VALUES__: {...} }`, because there's no way to tell from CSS custom
property names alone whether `foo-bar` is a sibling key or a nested
sub-key of `foo`.
This same ambiguity was already fixed for the CSS-embedded `theme()`
function in #19097 (which unwraps to the `DEFAULT` key when present),
and the changelog entry for that PR states it fixes this "in JS configs
**and plugins**" — but the fix only touched `apply-compat-hooks.ts`'s
`resolveThemeValue`, not `createThemeFn`'s `theme` function that's
exposed directly to plugins in `plugin-functions.ts`. This PR closes
that gap by applying the same DEFAULT-unwrapping there.
Without this fix, passing the raw object into `addUtilities` (a very
natural thing to do, since a plugin author expects a string) produces
broken CSS — the reserved `DEFAULT` key gets mangled into a garbage
property name (`-d-e-f-a-u-l-t`) by the kebab-case conversion, and the
internal `__CSS_VALUES__` bookkeeping leaks into the generated
stylesheet.
### Minimal reproduction
```js
// tailwind.config.js (registered via @config, or any @plugin-registered plugin)
const plugin = require('tailwindcss/plugin')
module.exports = {
plugins: [
plugin(function ({ addUtilities, theme }) {
addUtilities({
'.example-foo': { color: theme('colors.foo') },
})
}),
],
}
```
```css
@import "tailwindcss";
@config "./tailwind.config.js";
@theme {
--color-foo: red;
--color-foo-bar: blue;
}
```
**Before:**
```css
.example-foo color {
-d-e-f-a-u-l-t: red;
bar: blue;
}
.example-foo color __CSS_VALUES__ {
-d-e-f-a-u-l-t: 0;
bar: 0;
}
```
**After:**
```css
.example-foo {
color: red;
}
```
## Test plan
- Added a regression test in
`packages/tailwindcss/src/compat/plugin-api.test.ts` ("theme() resolves
the DEFAULT value when a bare CSS theme key shares a prefix with a
sibling key")
- Verified via `pnpm --filter tailwindcss exec vitest run` that this
test fails with the exact broken output shown above when the fix is
reverted, and passes once it's applied
- Ran the full `tailwindcss` package test suite (`pnpm --filter
tailwindcss exec vitest run`) — 4684 tests passing, no regressions
- Verified formatting on the changed files with `npx prettier --check`
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
## Description
Firefox's UA stylesheet already sets `iframe:focus-visible {
outline-style: none; }`, so Preflight's `:-moz-focusring { outline:
auto; }` rule overrides that and applies an unwanted auto outline to
focused iframes.
Adding `:where(:not(iframe))` to the selector preserves the improved
focus ring behavior for all other elements while respecting Firefox's
native iframe focus styling.
Fixes#19795
Edit by @RobinMalfait
## Test plan
Before:
<img width="1054" height="383"
alt="file-f833142116d36e1e620290b97455f001"
src="https://github.com/user-attachments/assets/81aa170b-2cef-4964-934d-61f258335e1a"
/>
After:
<img width="1056" height="310"
alt="file-400d9214fedf5b43693e10580a4869de"
src="https://github.com/user-attachments/assets/8f8bb81d-6070-425d-8820-327bf9e88e79"
/>
---------
Co-authored-by: root <root@localhost.localdomain>
Co-authored-by: Kirk Loretz <kirk-loretz-fsn@users.noreply.github.com>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This PR bumps most of the dependencies to the latest version.
This also marked some dependencies using a range such that you get
updates for free during the installation:
```diff
catalog:
- enhanced-resolve: 5.21.6
+ enhanced-resolve: ^5.24.1
- vite: 8.0.14
+ vite: ^8.1.2
- webpack: 5.107.0
+ webpack: ^5.108.3
```
Fixes: #20291
## Test plan
1. All tests on all OSes still pass [ci-all]
This PR fixes an issue where hex-based colors in arbitrary properties
and values were considered case-sensitive even though they are
case-insensitive in CSS.
If you look at the linked issue, there is this input CSS:
```css
@theme {
--color-brand-purple: #3f3cbb;
}
```
We expect that both `bg-[#3f3cbb]` and `bg-[#3F3CBB]` get canonicalized
to `color-brand-purple` but before this pr, only the first one would get
canonicalized that way (since it's a perfect match).
Technically a bunch more values are case-insensitive but a lot of them
_are_ sensitive so to get this 100% correct, a lot more parsing needs to
happen. I think we can start with this and expand the logic when needed.
Fixes: #20295
## Test plan
1. Added a regression test based on the linked issue
2. Added tests for arbitrary properties (`[color:#fff]` vs
`[color:#FFF]`), and tests for arbitrary properties (`bg-[#fff]` vs
`bg-[#FFF]`)
This PR re-adds the `--poll` option to the `@tailwindcss/cli` that we
had in Tailwind CSS v3, but didn't in Tailwind CSS v4.
In Tailwind CSS v4, we started using `@parcel/watcher` instead of
chokidar for our watcher in the CLI. However, this currently doesn't
support a `--poll` option.
This PR implements our own `--poll` option such that you can use it in
environments where fs events don't work properly (e.g. Docker).
Polling can be enabled by using `--watch --poll`, in this case we will
poll every `250ms` (I'm open for a different default value). You can
also pick your own interval by using `--watch --poll 500` which is
defined in milliseconds.
The `--poll` option will be less efficient than a normal `--watch`. But
if you are in a situation where you can't use `--watch` on its own then
this is a good fallback.
One thing you can do today is run the build command manually. If you do
have some tooling that _does_ work on your machine (such as
[`watchexec`](https://github.com/watchexec/watchexec)) then you can
automatically perform a full build. The biggest downside of this
approach is that you are doing a full build every time, instead of an
incremental build.
With this PR, we try to fix that by still allowing incremental builds.
This should result in the same behavior as the normal `--watch`
function:
1. First run, will trigger a full build
1. When any of the source files changes:
- If all classes were already known, then it will be a no-op, but you
will see a log in the terminal about it.
- If a new class is detected, then the CSS will be updated, but it will
be much more efficient than a full rebuild
1. When the input CSS file changes, or any of its dependencies, then a
full rebuild will be triggered (such that your new `@utility` are
available, and `@theme` values are updated).
The implementation is a little bit more complex just because I didn't
want to spam the terminal output even if we are polling every `250ms`.
In the Oxide scanner we do track the modified times of each file. Every
`250ms` we traverse the file system and skip the files that we know
didn't change (since the mtime is the same). If the file was touched,
then we will parse it again to extract possible Tailwind CSS classes. We
will also track which files were scanned such that we can know whether
we have to trigger a full-rebuild or not (in case the input.css file or
any of its dependencies was changed).
Fixes: #18109Fixes: #18540Fixes: #15750
## Test plan
1. Existing tests pass
2. An integration test has been added for the `--poll` option
3. Tested it on the tailwindcss.com codebase:
<img width="679" height="320" alt="image"
src="https://github.com/user-attachments/assets/af858da5-3e07-448d-86bd-8eacdf3bf8d1"
/>
Annotated:
```
≈ tailwindcss v4.3.2
Done in 105ms Initial build
Done in 3ms Saved a file that resulted in a no-op
Done in 2ms Saved a file that resulted in a no-op
Done in 3ms Saved a file that resulted in a no-op
Done in 53ms Saved a file with a new class
Done in 2ms Saved a file that resulted in a no-op
Done in 2ms Saved a file that resulted in a no-op
Done in 3ms Saved a file that resulted in a no-op
Done in 88ms Saved a the input.css file
Done in 3ms Saved a file that resulted in a no-op
Polling for changes…
```
We check the file system every `250ms` by default, but we won't log to
prevent spamming the terminal.
This PR fixes an issue where new PostCSS release could lead to type
related issues if newer versions change the types.
Right now `@tailwindcss/postcss` uses a hardcoded PostCSS version. Let's
loosen this up and use a semver range instead.
Fixes: #20288
## Test plan
1. All tests still pass
This PR improves the release script:
- The `node-linker=hoisted` was missing for the `wasm32-wasi` package
(this is because of the pnpm v11 changes where it migrated the
noide-linker setup from `.npmrc` to a flag during `pack`)
- Notify discord in case the release fails
## Test plan
1. All tests should pass because we didn't change anything related to
code
2. Unfortunately, the only way to properly testing this is to merge it
Sometimes this is flaky on CI, even though it doesn't make any sense
because we use `setTimeout(_, 500)` which should _at least_ result in
500ms...
But sometimes this results into:
```
AssertionError: expected 499.76 to be greater than or equal to 500
```
This PR bumps the repo to pnpm v11 (from v9).
I kept running into weird Windows specific issues for the integration
tests due to some shims but they all pass right now.
Bumping to pnpm v11 also meant that everything is driven by a
`pnpm-workspace.yaml` file, and changes applied via the `pnpm` field in
the `package.json` don't work anymore.
This also moved the `node-linker` setup that was defined in the `.npmrc`
file for the wasm oxide build into the `pnpm-workspace.yaml` file.
Ideally this is scoped to just this package, but I couldn't get that to
work, so it's applied to all packages right now.
[ci-all]
This PR ignores Lightning CSS warnings for the unknow at-rule
`@position-try`.
This is a temporary fix/workaround until support for `@position-try`
lands in Lightning CSS:
https://github.com/parcel-bundler/lightningcss/pull/1238Fixes: #20275
## Test plan
Ran a test based on the issue:
Before:
```shellsession
❯ tw -i x.css -o out.css --optimize
≈ tailwindcss v4.3.1
Found 1 warning while optimizing generated CSS:
│ position-try-fallbacks: --flip-above;
│ }
│ @position-try --flip-above {
┆ ^-- Unknown at rule: @position-try
┆
│ top: auto;
│ bottom: 0;
Done in 29ms
```
After:
```shellsession
❯ tw -i x.css -o out.css --optimize
≈ tailwindcss v4.3.1
Done in 14ms
```
In newer Vite versions this extension matters, without the
vite/resolvers integration tests won't work.
Since this is still a placeholder fake file where Vite will run
`dirname` on, the actual file doesn't really matter as long as it's
relative to this file.
This PR handles template toolkit syntax as a pre-processor step such
that `%]` and `[%` are seen as valid boundary characters.
This is handled for the `.tt`, `.tt2` and `.tx` file extensions. It's
not handled if this syntax is used in `.html` files because then
everybody pays a pre processor cost even if you don't need this syntax
in most cases.
This now ensures that a `template.tx` like this:
```html
<div class="[% IF $is_open %]bg-white/40[% ELSE %]bg-white/10[% END %]"></div>
<!-- ^^^^^^^^^^^ ^^^^^^^^^^^ -->
```
Extracts the classes in between those conditions correctly.
This also fixes a small issue related to Maud, a template engine for
Rust where conditionals like `p.text-black[condition]` caused the
`text-black` class not to be extracted. This is fixed as part of this PR
because it was commented on the linked issue.
Fixes: #20233
## Test plan
1. Added a new extractor
2. Added regression tests
3. All existing tests pass
This PR fixes an issue where a `@source` pointing to a concrete file
could result in scanning the entire parent folder instead of only
looking for the file we are actually interested in.
When we optimize a `@source`, we move all the static parts of the
pattern to the `base`. This means that a `@source` like this:
```css
@source "../../app.config.ts";
```
Resolves to:
```rs
SourceEntry::Pattern { base: "/Users", pattern: "/app.config.ts" }
```
When walking the `base`, we would only emit an `!app.config.ts` rule.
This means that _everything_ in the `/Users` folder is still walked, and
the result is then thrown away. If you take a look at the `gitignore`
equivalent (which is what we build behind the scenes), then we would
essentially create the following:
```gitignore
!app.config.ts
```
But if you know how `gitignore` files work, then you know that this does
force `app.config.ts` to _not_ be ignored, but it doesn't say anything
about all the other files/folders.
The fix is to restrict the `base` so that we ignore everything in the
folder, and then explicitly re-include only the pattern we care about.
Fixing this would result in the following:
```gitignore
*
!foo.ts
```
In the reproduction from #20255 this takes the build from appearing to
hang (~22s) down to ~20ms on my machine.
There are a few edge cases we have to be careful about:
**Multiple patterns for the same base.** If we have multiple `@source`
directives for the same folder:
```css
@source "./src/foo.ts";
@source "./src/bar.ts";
```
Then blindly emitting `*` for each one would result in this `.gitignore`
equivalent:
```gitignore
*
!foo.ts
*
!bar.ts
```
Notice that the second `*` would end up ignoring `foo.ts` again. To
avoid this, we only emit the `*` rule once per `base`.
**Dynamic parts in intermediate folders.** If the pattern still contains
a `*` in one of its folders:
```css
@source "./src/ba*/*.html";
```
This resolves to:
```rs
SourceEntry::Pattern { base: "/src", pattern: "/ba*/*.html" }
```
If we now inject the `*` rule for `/src`, then we would never walk the
`ba*` folders (e.g. `bar` or `baz`). To fix this, we add inverse rules
for each parent segment of the pattern:
```gitignore
* ← ignore everything
!/ba*/ ← except for the `ba*/` folders, so we walk into them
!/ba*/*.html ← then scan the `*.html` files in them
```
**Bases already covered by a broader source.** If the `base` is already
included (or nested) under an unrestricted source (an `Auto`/`External`
source, or a `Pattern` containing `**`), then we leave it alone.
Restricting it would incorrectly hide siblings that the broader source
is supposed to pick up. For example:
```css
@source "**/*";
@source "./src/components/button.html";
```
Here the `**/*` source should keep auto-detecting every file, so we must
_not_ restrict `src/components` just because there's a more specific
`@source` pointing to it.
Source order is preserved throughout, so later `@source not …` rules can
still override an earlier restricted source.
Fixes: #20255
## Test plan
1. Added unit tests realted to this `sources` logic
2. Added integration like tests for the scanner itself
3. All other tests should pass as-s
4. Should work on each OS [ci-all]
This PR fixes an issue where intellisense recommends this
canonicalization:
```diff
- text-[calc(var(--spacing)*4)]
+ text-[--spacing(4)]
```
Which is correct, but the issue is that the result is different due to
ambiguity of the `text-*` utilities.
```css
.text-\[calc\(var\(--spacing\)\*4\)\] {
font-size: calc(var(--spacing) * 4);
}
.text-\[--spacing\(4\)\] {
color: calc(var(--spacing, 0.25rem) * 4);
}
```
Notice that we're using `color` all of a sudden? This is because that's
the default and we infer the data type based on the arbitrary value. The
`calc(…)` infers that the type is `length`, but we don't know what the
type of `--spacing(…)` is so we fallback to the default type which would
be `color`.
This PR makes sure that built in functions like `--alpha(…)` and
`--spacing(…)` are resolved as `color` and `length` respectively.
With this fix in place, this is the result:
```css
.text-\[calc\(var\(--spacing\)\*4\)\] {
font-size: calc(var(--spacing) * 4);
}
.text-\[--spacing\(4\)\] {
font-size: calc(var(--spacing, 0.25rem) * 4);
}
```
Fixes: #20256Fixes: #20258
## Test plan
1. Added a regression test to make sure this doesn't happen anymore
This PR doesn't fix any issues, but it does add an integration test (as
a regression test) to make sure that `@variant` with default variants
and custom variants can be used inside of JS based plugin APIs.
In Tailwind CSS v4.3.1 we introduced a PR that handles `@variant` in the
`addBase` Plugin API
(https://github.com/tailwindlabs/tailwindcss/pull/19480). This was a bit
of an older PR, but the tests made sense, so it was merged.
However, by introducing that PR, we introduced a bug that the `@variant`
was handled too early. If you added custom variants later _and_ used it
in the `addBase`, then you would get an error since the variant isn't
available (yet).
That issue was fixed by
https://github.com/tailwindlabs/tailwindcss/pull/20247
Now the question remains, why did we even have the original PR when it
already worked?
The use case we had was using `@variant` as part of the
`@tailwindcss/typography` plugin configuration for one of our templates.
I was indeed able to reproduce the issue where `@variant lg` was seen in
the output CSS file.
Turns out that this template was using `@tailwindcss/typography` +
`@variant` in the configuration, but it was also using Tailwind CSS
v4.1.15.
Upgrading to the latest version automagically fixed the issue we had.
This is also the behavior you can see in the integration test.
The correct behavior was introduced in an even older PR
https://github.com/tailwindlabs/tailwindcss/pull/19263
All that said, everything should work in the next release related to
`@variant` usages inside JS based APIs.
**Tiny improvement**
While debugging what's going on, I noticed that we looped over the AST
to get some nodes out and we did that twice. This PR also improves that
by re-using the same list of nodes instead of computing it twice. This
won't have a huge impact, but it happened while compiling every single
utility which is not ideal.
## Test plan
1. All tests should pass
2. I can't see `@variant` in the output CSS file
Input:
<img width="655" height="323" alt="image"
src="https://github.com/user-attachments/assets/20c8d524-3575-488c-b0c0-5c4669f37dc7"
/>
Before:
<img width="477" height="175" alt="image"
src="https://github.com/user-attachments/assets/045e2086-1d4b-487c-96ff-676351412935"
/>
After:
<img width="484" height="175" alt="image"
src="https://github.com/user-attachments/assets/24d950b1-7c28-40f8-96bc-81b38be0e7a3"
/>
This PR fixes an issue where `@variant` inside `addBase` is being used
with a custom variant.
The issue is that we substitute the `@variant` calls immediately when we
call the `addBase` function. That means that variants that aren't
processed yet will error out.
This is a regression, because this used to work in Tailwind CSS v4.3.0
and started failing in Tailwind CSS v4.3.1.
## Test plan
1. Added a regression test
This PR fixes an issue where if the `--input` file (when using
`@tailwindcss/cli`) lives in an ignored folder, then changes to the
input file won't trigger a rebuild.
This happened in #17632 where the input file lives in the `assets/`
folder which is ignored by default.
However, if you make a change to the input file, then the CLI doesn't
restart which means that you can't update the `@source` directives
without quitting the program and re-starting it manually.
This PR solves that by automatically injecting an `@source` with the
path to the input file. This way the CLI will see changes, even if the
input file is in an ignored directory.
Fixes: #17632
## Test plan
1. Added an integration test
2. Existing tests pass
3. Reproduced it on the
Before:
<img width="1122" height="1376"
alt="file-295ce4696b6b3199a3915c63f8bb50cd"
src="https://github.com/user-attachments/assets/09e5e965-38d1-474d-bfc6-3b5e9bca73b6"
/>
After:
<img width="1122" height="1376"
alt="file-301b2f2e06aee045957355014ae5de87"
src="https://github.com/user-attachments/assets/2b6d4cba-f456-4138-98bd-2259876fe70a"
/>
This PR might fix an issue where resolvers don't work on deno v2.8.x
(they do work on deno v2.7.x) when using `@tailwindcss/vite`.
This happens when the `context.parentURL` is not actually a URL at all,
and therefore the `new URL(…)` just crashes. This PR will fallback to
the incoming result for files that are not passed proper URLs.
Fixes: #20232
## Test plan
1. Existing tests pass
2. Can't seem to figure out how to test this in the reproduction issue
without publishing first. So going to merge this and test the insiders
version instead.
This PR adds bare value support for `auto-rows-*` and `auto-cols-*`.
We first introduced `auto-rows-auto`, `auto-rows-min`, `auto-rows-max`
and `auto-rows-fr` (same for `auto-cols-*`) back in Tailwind CSS v1.9
(https://v1.tailwindcss.com/docs/grid-auto-rows#app) but we haven't
touched it since.
This PR now adds support for bare values that use the spacing scale.
That means that you can now use `auto-rows-12` or `auto-cols-12` which
will result in the following CSS:
```css
.auto-cols-12 {
grid-auto-columns: calc(var(--spacing) * 12);
}
.auto-rows-12 {
grid-auto-rows: calc(var(--spacing) * 12);
}
```
We could also add support for percentage based values. The only question
is what the syntax should be. This can either be `auto-rows-3/4` or
`auto-rows-75%`.
For the fraction case, we already have `w-3/4` and `aspect-3/4` even
though they both have a different value as a result:
```css
.aspect-3\/4 {
aspect-ratio: 3/4;
}
.w-3\/4 {
width: calc(3 / 4 * 100%);
}
```
We also have some precedence for the `%` value as well, e.g. `via-10%`
for gradient color stops.
I think I would personally towards the `auto-rows-75%` value instead of
`auto-rows-3/4`. The fraction syntax works great for aspect ratio
because that's literally what it is (`aspect-16/9`). The fraction also
works great for widths, because you typically have 2 elements next to
each other:
```html
<div>
<div class="w-1/3"></div>
<div class="w-2/3"></div>
</div>
```
Since you only use the `auto-rows-*` once on a parent element, I think
the `auto-rows-75%` makes a bit more sense than `auto-rows-3/4` since
there is no _other_ element (at least not that I can think of).
## TODO
- [ ] Support percentages?
- [ ] Syntax A: `auto-rows-3/4`
- [ ] Syntax B: `auto-rows-75%`
- [ ] Syntax C: support both, but that seems silly
Percentage support isn't a blocker for this PR, since you can always use
`auto-rows-[75%]` if you want
## Test plan
1. Added a test to prove that this works
1. Existing tests still pass
Requested by:
https://github.com/tailwindlabs/tailwindcss/discussions/20225
This PR fixes an issue on Windows where the `@tailwindcss/cli` with the
`--watch` flag crashes when using a `@source` with a base path that
doesn't exist on disk.
This happens when setting up the `@parce/watcher` for directories that
don't exist. This PR essentially filters out these directories that
don't exist on disk to prevent the crash.
It might be that if you add the folder _later_, while the watcher is
already watching, that you have to restart the `@tailwindcss/cli` (or
save the `index.css` (the file that contains the `@source` directives),
this also recreates watchers from scratch).
If this issue causes problems for `@tailwindcss/postcss` and
`@tailwindcss/vite` in the future as well, then we can move this logic
back to Oxide. We do maintain the incoming `@source` files as best as
possible without resolving to absolute paths. Back when we did resolve
them, if that process error'd we just never returned the glob.
The root cause is still referencing folders that don't exist.
Fixes: #20231
## Test plan
1. Added a failing test, that did fail on Windows
2. Once fixed, all tests should pass
[ci-all]
---------
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
## Summary
Fixes#20219.
The `@tailwindcss/postcss` exports map serves the CJS-shaped
`dist/index.d.ts` (`export = _default`) for both the `import` and
`require` conditions, while the correct ESM declaration file
`dist/index.d.mts` (`export { _default as default }`) is built and
published but never referenced. Type checkers that treat the import
condition as ESM reject the default import with TS1192 — concretely,
`deno check` on Deno 2.8.3+ (which ships TypeScript 6.0) fails on
`import tailwindcss from "@tailwindcss/postcss"`.
This splits the export entry into per-condition `types` blocks so ESM
importers resolve `index.d.mts` and CJS consumers keep `index.d.ts` —
the same shape `@tailwindcss/vite` already uses (`"types":
"./dist/index.d.mts"`).
Two notes on the issue as filed:
- Stock `tsc` with NodeNext does **not** reproduce the error
(declaration file format follows the file extension and package `type`,
not the matched condition), so I didn't take the suggested `export =` →
`export default` change in `index.d.ts` — that would break CJS
consumers, and the correct ESM declarations already ship.
- `@tailwindcss/node` has the same single-`types` exports shape; happy
to follow up there if you want parity.
## Test plan
Against `@tailwindcss/postcss@4.3.0` with the published exports map,
then again with only this `package.json` change applied to the installed
package:
```sh
# Deno 2.8.3 (TypeScript 6.0), nodeModulesDir: auto
deno check main.ts # before: TS1192 "Module ... index.d.ts has no default export" — after: passes
```
```sh
# typescript@6.0.0-dev (NodeNext), one ESM importer + one .cts require importer
tsc --noEmit # passes both before and after (no regression for npm consumers)
```
`@arethetypeswrong/cli`:
| | published 4.3.0 | with this change |
|---|---|---|
| node16 (from ESM) | 🎭 Masquerading as CJS | 🟢 (ESM) |
| node16 (from CJS) | 🟢 (CJS) | 🟢 (CJS) |
| bundler | 🟢 | 🟢 |
This PR fixes a bug where we suggest a canonicalization with a high
precision number.
E.g.:
`w-[calc(100%/3.5)]` → `w-[28.571428571428573%]`
While this is technically correct, it's also not as user friendly. This
PR solves this issue by checking whether the result has a precision of
`.xx` at most. If that's not the case, then we keep the original
expression.
Fixes:
https://github.com/tailwindlabs/tailwindcss-intellisense/issues/1591
## Test plan
1. Added an a regression test for this situation
2. Existing tests pass
## Summary
Fixes a double word typo in a test comment: 'No need to to wrap' → 'No
need to wrap'.
## Test plan
This is a comment-only change in a test file. No functional changes.
Co-authored-by: Codex <codex@openai.com>