Commit graph

1091 commits

Author SHA1 Message Date
Robin Malfait
bdcd7087b3
Don't trigger a full page reload for scanned files that Vite processes as modules (#20336)
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: #20320
Fixes: #19903
Closes: #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"
/>
2026-07-15 17:49:05 +02:00
Robin Malfait
f861d5c5e6
use the same fix for template files
Forgot this as part of https://github.com/tailwindlabs/tailwindcss/pull/20329
2026-07-14 18:42:33 +02:00
Benoît Rouleau
a854b35bcf
Prevent @tailwindcss/upgrade from rewriting files inside node_modules when run from a workspace subpackage (#20329)
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>
2026-07-14 16:39:24 +00:00
Robin Malfait
e48c5e8047
Fix weird character rendering on Windows with Japanese locale (#20318)
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: #19767
Fixes: #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...
2026-07-14 17:04:20 +02:00
Robin Malfait
b03e5e7731
Lazy load @parcel/watcher (#20325)
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"
/>
2026-07-13 15:53:42 +02:00
Robin Malfait
35a3e9c515
Always produce <length> value when optimizing --spacing(0) (#20319)
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: #20315
Closes: #20317

## Test plan

1. Manually tested that now `--spacing(0)` does produce `0px` which has
a length type
2026-07-09 18:34:27 +02:00
Robin Malfait
4af47fbe94
Fix hues in achromatic theme colors to be none (#20314) 2026-07-09 13:36:34 +02:00
Robin Malfait
5835691d21
Handle CSS nesting natively (#20124)
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
2026-07-07 18:28:41 +02:00
Lazizbek Ergashev
9b0e8af258
Ensure @tailwindcss/postcss rebuilds when the input CSS changes but its mtime is unchanged (#20310)
## 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>
2026-07-06 11:32:20 +00:00
Robin Malfait
67c745efde
Fix bug in attribute selector parsing (#20303)
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
2026-07-03 21:13:13 +02:00
한국
2683903b86
Support fractional opacity modifiers for named shadow sizes (#20302)
## 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>
2026-07-03 12:34:02 +00:00
한국
04588b1e8f
Fix theme() in JS plugins returning unresolved object instead of DEFAULT value (#20299)
## 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>
2026-07-02 13:20:14 +00:00
highoncomputers
b53fa096c9
fix: exclude iframes from focus-visible auto outline in Preflight (#20292)
## 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>
2026-07-02 10:39:28 +00:00
Robin Malfait
ef79119d4e
Bump dependencies (#20300)
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]
2026-07-02 11:45:49 +02:00
Robin Malfait
e46b3d74ee
Canonicalization: make hex colors case insensitive (#20298)
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]`)
2026-07-02 00:04:44 +02:00
Robin Malfait
39656f7d9a
Add --poll option to @tailwindcss/cli (#20297)
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: #18109
Fixes: #18540
Fixes: #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.
2026-07-01 21:25:26 +02:00
Robin Malfait
056a155072
4.3.2 (#20281) 2026-06-29 10:10:00 -04:00
Robin Malfait
c8b081d963
Add suggestions for named opacity modifiers (#20287)
This PR re-enables suggestions for named opacity modifiers that can be
configured via the `--opacity-*` namespace.

Before we launched v4, we got rid of this feature:
https://github.com/tailwindlabs/tailwindcss/pull/14278 &
https://github.com/tailwindlabs/tailwindcss/pull/14339
But later we re-added the feature, without updating intellisense
suggestions: https://github.com/tailwindlabs/tailwindcss/pull/15009

This PR re-adds suggestions for utilities that read from the
`--opacity-*` namespace for named modifiers. A lot of utilities use some
internal abstraction that get this for free once we use the
`colorUtility` setup, but some utilities don't use these, and I had to
add the `--opacity` modifier theme key manually.

Fixes:
https://github.com/tailwindlabs/tailwindcss-intellisense/issues/1541

## Test plan

1. Update intellisense related tests now that we read from the
`--opacity-*` namespace
2. All tests pass

Before:
<img width="1122" height="1376"
alt="file-5462b830c5ebbd6046494713344ff5ff"
src="https://github.com/user-attachments/assets/ab0771c1-da8d-4179-b19d-183c2c567606"
/>


After:

<img width="1122" height="1376"
alt="file-9247e05165f72e8bb974673ee17bd35a"
src="https://github.com/user-attachments/assets/2afdd50f-6579-4a4c-bf9b-8d402fcf1184"
/>

Notice that the `/foo` suggestion is available
2026-06-29 15:35:45 +02:00
Robin Malfait
94f7907b55
add margin of error to the test
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
```
2026-06-25 19:23:36 +02:00
Robin Malfait
7ff413bad4
Ignore unknown at-rule warnings for @position-try (#20277)
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/1238

Fixes: #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
```
2026-06-25 13:21:12 +02:00
Robin Malfait
bb6a10937c
use .ts instead of .css
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.
2026-06-25 13:03:10 +02:00
depfu[bot]
c33b85d012
Update all pnpm dependencies (2026-06-19) (#20262)
This is your weekly update of **all** pnpm dependencies. Please take a
good look at what changed and the test results before merging this pull
request.

### What changed?

✳️ postcss-selector-parser (7.1.1 → 7.1.4, patch) ·
[Repo](https://github.com/postcss/postcss-selector-parser) ·
[Changelog](https://github.com/postcss/postcss-selector-parser/blob/master/CHANGELOG.md)
·
[Release](https://github.com/postcss/postcss-selector-parser/releases/tag/7.1.4)
·
[Diff](cf6637ed6a...4a7e4e3685)

✳️ semver (7.8.1 → 7.8.4, patch) ·
[Repo](https://github.com/npm/node-semver) ·
[Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md) ·
[Release](https://github.com/npm/node-semver/releases/tag/v7.8.4) ·
[Diff](76416081a8...8640bd68f1)

✳️ turbo (2.9.14 → 2.9.18, patch) ·
[Repo](https://github.com/vercel/turborepo) ·
[Changelog](https://github.com/vercel/turborepo/blob/main/RELEASE.md) ·
[Release](https://github.com/vercel/turborepo/releases/tag/v2.9.18) ·
[Diff](fc62fe0d9c...3bdce3277d)




---
![Depfu
Status](https://depfu.com/badges/edd6acd35d74c8d41cbb540c30442adf/stats.svg)

[Depfu](https://depfu.com) will only send you the next scheduled PR once
you merge or close this one.

<details><summary>All Depfu comment commands</summary>
<blockquote><dl>
<dt>@​depfu refresh</dt><dd>Rebases against your default branch and
redoes this update</dd>
<dt>@​depfu recreate</dt><dd>Recreates this PR, overwriting any edits
that you've made to it</dd>
<dt>@​depfu merge</dt><dd>Merges this PR once your tests are passing and
conflicts are resolved</dd>
<dt>@​depfu cancel merge</dt><dd>Cancels automatic merging of this
PR</dd>
<dt>@​depfu close</dt><dd>Closes this PR and deletes the branch</dd>
<dt>@​depfu reopen</dt><dd>Restores the branch and reopens this PR (if
it's closed)</dd>
</dl></blockquote>
</details>

Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>
2026-06-19 13:11:16 +02:00
Robin Malfait
c46f654fa0
Ensure --alpha(…) is seen as a color, and --spacing(…) is seen as a length (#20260)
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: #20256
Fixes: #20258

## Test plan

1. Added a regression test to make sure this doesn't happen anymore
2026-06-18 15:02:18 +02:00
Robin Malfait
5e9f66e428
Ensure @variant can be used in JS based APIs (#20252)
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"
/>
2026-06-17 15:23:20 +02:00
Robin Malfait
707c23b955
Ensure custom variants can be used via @variant in addBase (#20247)
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
2026-06-16 23:05:50 +02:00
Robin Malfait
cc3b634fe0
Ensure @tailwindcss/cli watches the input file (#20246)
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"
/>
2026-06-16 18:38:29 +02:00
Robin Malfait
51f0760d29
Fix crash when using @tailwindcss/vite using deno v2.8.x (#20245)
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.
2026-06-16 16:03:27 +02:00
Robin Malfait
127d17033a
Add bare value support for auto-rows-* and auto-cols-* (#20229)
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
2026-06-16 11:50:35 +02:00
Robin Malfait
abc4670a9a
Prevent crash when using @tailwindcss/cli using --watch on Windows (#20242)
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>
2026-06-15 17:20:31 +02:00
depfu[bot]
d1f9f53bf9
Update all pnpm dependencies (2026-06-15) (#20241)
This is your weekly update of **all** pnpm dependencies. Please take a
good look at what changed and the test results before merging this pull
request.

### What changed?

✳️ @emnapi/core (1.10.0 → 1.11.1, minor) ·
[Repo](https://github.com/toyobayashi/emnapi) ·
[Release](https://github.com/toyobayashi/emnapi/releases/tag/v1.11.1) ·
[Diff](ba84999164...6c3668c9af)

✳️ @emnapi/runtime (1.10.0 → 1.11.1, minor) ·
[Repo](https://github.com/toyobayashi/emnapi) ·
[Release](https://github.com/toyobayashi/emnapi/releases/tag/v1.11.1) ·
[Diff](ba84999164...6c3668c9af)

✳️ @tailwindcss/typography (0.5.19 → 0.5.20, minor) ·
[Repo](https://github.com/tailwindlabs/tailwindcss-typography) ·
[Changelog](https://github.com/tailwindlabs/tailwindcss-typography/blob/main/CHANGELOG.md)
·
[Release](https://github.com/tailwindlabs/tailwindcss-typography/releases/tag/v0.5.20)
·
[Diff](e002ab89ad...e3714a3fe5)

✳️ @emnapi/wasi-threads (1.2.1 → 1.2.2, patch) ·
[Repo](https://github.com/toyobayashi/emnapi)




---
![Depfu
Status](https://depfu.com/badges/edd6acd35d74c8d41cbb540c30442adf/stats.svg)

[Depfu](https://depfu.com) will only send you the next scheduled PR once
you merge or close this one.

<details><summary>All Depfu comment commands</summary>
<blockquote><dl>
<dt>@​depfu refresh</dt><dd>Rebases against your default branch and
redoes this update</dd>
<dt>@​depfu recreate</dt><dd>Recreates this PR, overwriting any edits
that you've made to it</dd>
<dt>@​depfu merge</dt><dd>Merges this PR once your tests are passing and
conflicts are resolved</dd>
<dt>@​depfu cancel merge</dt><dd>Cancels automatic merging of this
PR</dd>
<dt>@​depfu close</dt><dd>Closes this PR and deletes the branch</dd>
<dt>@​depfu reopen</dt><dd>Restores the branch and reopens this PR (if
it's closed)</dd>
</dl></blockquote>
</details>

---------

Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2026-06-15 13:13:32 +00:00
Robin Malfait
8a14a71010
4.3.1 (#20226) 2026-06-12 19:33:35 +02:00
Truffle
522288ca08
Serve ESM type declarations to ESM importers of @tailwindcss/postcss (#20228)
## 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 | 🟢 | 🟢 |
2026-06-12 14:33:16 +00:00
Robin Malfait
12833aa4b3
Fix canonicalization bug where we end up with a high precision number (#20221)
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
2026-06-12 00:23:11 +02:00
Rohan Santhosh Kumar
97a5b3abfb
docs: fix double word 'to to' in test comment (#20216)
## 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>
2026-06-11 12:07:00 +02:00
Robin Malfait
d01e103cc4
Add missing inset keyword for inset-shadow-none (#20208)
This PR fixes an issue where the `inset-shadow-none` class did not use
the `inset` keyword. This PR fixes that by introducing the `inset`
keyword:

```diff
  .inset-shadow-none {
-   --tw-inset-shadow: 0 0 #0000;
+   --tw-inset-shadow: inset 0 0 #0000;
    box-shadow: var(--tw-inset-shadow), var
  }
```

While the end effect is the same (there will be no shadow), it also
means that we switch between the types of shadows. This results in the
fact that things like transitions don't behave the way they should.

A minimal reproduction looks like this:
https://play.tailwindcss.com/iOrnKWP49a (depending on when you visit
this link, it might be fixed)

## Test plan

1. Updated tests to reflect
2. Verified in the Vite playground that this is the case. In the video
you will notice that the transition is smooth in the fixed case, but
there is no transition in the current state:



https://github.com/user-attachments/assets/15d8c78e-6be5-4c6c-8df9-1044c14e8f39
2026-06-08 23:08:52 +02:00
Robin Malfait
68fcdf1f05
Do not crash when migrating empty classes (#20205)
This PR fixes a crash while using `npx @tailwindcss/upgrade` when
migrating classes with no body inside of an `@layer utilities`.

When running `npx @tailwindcss/upgrade`, one thing we do is migrate the
CSS from:
```css
@layer utilities {
  .foo {
    color: red;
  }
}
```
To:
```css
@utility foo {
  color: red;
}
```

We already have some logic that leaves non-classe (IDs, attribute
selectors, ...) alone. But if we are migrating a class that has no body,
then we will migrate that as well:
```css
@layer utilities {
  .empty {
  }
}
```
Is turned into:
```css
@utility empty {
}
```

But later in the migration process this will result in an error because
a `@utility` has to have at least _some_ nodes.

Ideally, you don't even have CSS that has empty rules since it doesn't
have any effect in the browser (except of making your CSS file bigger),
but it could be that you don't have control over this file, so a fix is
still valid.

This PR solves that by leaving those rules alone, and keep them in an
`@layer utilities`.

Fixes: #20204

## Test plan

1. Added a dedicated test for this usecase
2026-06-07 21:50:35 +02:00
Jordan Pittman
ad6693906a
Allow @variant to be used inside addBase (#19480)
This seems like an unintentional omission

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2026-06-04 10:46:40 +00:00
Aarni Koskela
efae52c3af
Simplify CSS when using utilities that use a *-0 or *-1 value (#20196)
## Why?


While inspecting a Tailwind-powered site, I noticed selectors like

```css
.inset-0{inset:calc(var(--spacing) * 0)}
.inset-x-0{inset-inline:calc(var(--spacing) * 0)}
.top-0{top:calc(var(--spacing) * 0)}
```

which seem a bit silly, not to mention more complex for the end-user
device parsing the CSS.

## Summary

This PR adjusts helpers to not generate `calc(... * 0)` expressions.

https://github.com/tailwindlabs/tailwindcss/pull/19095 does a similar
thing on CSS AST, but that doesn't run during the build proper.

## Test plan

Ran `pnpm build && pnpm test && pnpm test:integration`. (Some unrelated
tests failed on my machine, hopefully less in CI.)

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2026-06-03 16:44:09 +00:00
Robin Malfait
0c5f8bfd88
Ignore the standalone CLI (#20139)
This PR fixes an issue where if you use the standalone CLI, and you move
the standalone CLI into the current project, then we would scan that
standalone CLI as-if it contains Tailwind CSS classes. Since the CLI
contains actual Tailwind CSS classes, and is in fact readable text, this
binary would've been used as a source.

There are a few ways of fixing this, we could hardcode all the known
names, but that would result in an issue if you rename the CLI. We could
check whether it's a binary format and look for magic numbers at the
top. We could also check for a shebang at the top of the file and skip
it that way.

While some of these solutions might still be useful for the future. For
now I fixed it by essentially always ignoring `process.execPath`. That
way we never ever scan the actual executable regardless of whether you
renamed it or not.

Fixes: #20134

## Test plan

- Added an integration tests
- Works on every OS [ci-all]
2026-06-02 12:49:53 +02:00
Robin Malfait
44818a6cf6
Ensure @tailwindcss/cli recovers from a deleted transitive dependency (#20137)
This PR fixes an issue where the `@tailwindcss/cli` can get into a
non-recoverable state when any of the transitive dependencies break.

Tailwind CSS has 2 kinds of dependencies:

1. All your templates
2. All dependencies that contribute to your configuration such as the
`input.css`, any plugins, any `tailwind.config.js` files and so on.

When a template changes, we just have to scan for new Tailwind CSS
classes and emit a new CSS file. But when the `input.css` file, or any
of its dependencies changes, then we want to perform a full rebuild.

The idea is that your `@theme` might have changed, or new plugins have
been added, or old plugins have been removed.

If you have an `input.css` file:
```css
@import "tailwindcss";
@config "./tailwind.config.js";
```

That relies on a custom config: `tailwind.config.js`:
```js
const theme = require('./my-custom-theme.js');
module.exports = {
  theme
}
```

If that file relies on yet another file: `./my-custom-theme.js`, then
changes there should also trigger a full rebuild.

Since we're dealing with JavaScript here, we want to clear the require
cache and rebuild the dependency tree such that another change to any of
these files triggers a full fresh build.

However, if any of those (transitive) dependencies are deleted, then we
will end up in an invalid state. Creating a new compiler will result in
a build error. The compiler won't be able to figure out the entire
dependency tree, and we're stuck.

Once the user fixes the potentially missing dependency, the watchers
will not be watching any of those files because we created a fresh
compiler.

With this PR, we fix that by keeping track of old paths and using those
while we are still in an invalid state. The moment everything is fixed,
a fresh dependency tree is created and everything starts working again
without you having to restart the `@tailwindcss/cli` command.

Fixes: #20113
Closes: #20114
Closes: #20133

## Test plan

- Added an integration test that removes the transitive dependency.
Re-adding that file later will recover the CLI state.
2026-06-01 19:15:40 +02:00
Robin Malfait
6b43b6400a
Canonicalization: limit arbitrary to bare values conversion (#20130)
This PR improves the canonicalization process by limiting the bare
values to a certain amount.

Before this PR, whenever we have an arbitrary value, e.g. `left-[6px]`,
then we prefer to use a bare value instead e.g. `left-1.5`. In most
cases, this makes sense.

However, there are places where this doesn't really make sense
(https://x.com/kettanaito/status/2059987396050268589)

- `left-[99999px]` → `left-24999.75 `

The hard part is to figure out _why_ this feels wrong. The `.75` could
feel wrong, but in the `left-[6px]` → `left-1.5`, the `.5` makes sense.
If we reduce that big number to `left-[99996px]` → `left-24999`, then
there is no floating point but it still feels wrong.

One possibility I can think of is to analyze the incoming value and see
if we find certain patterns. All repeating numbers, fun numbers like
`1337`, common numbers most programmers know such as `720px`, `1280px`,
etc.

But instead of that, I think it's more reasonable to limit the bare
value such that the `px` based value doesn't exceed a big number. We can
improve the logic if there are other cases that don't really make sense.

The biggest value we have in our default theme is `--breakpoint-2xl:
96rem`, which is equivalent to `1536px`.

So I think any bare value that results in a value `<= 1536px` should
probably be fine.

In this case, `left-[99999px]` would stay as `left-[99999px]`, but
`left-[6px]` is still converted to `left-1.5`.

Note: this is only happening for arbitrary values being converted to
bare values _if_ they use the `--spacing` variable internally.

Values such as `z-[99999999999]` will still be converted to
`z-99999999999`, since the intent is still clear.

## Test plan

1. Added new tests for these limitations
2. Other existing tests still pass
2026-05-29 21:31:47 +02:00
Robin Malfait
d4f24c5f16
Fix invalid canonicalization where 0<unit> was migrated to 0 (#20127)
This PR fixes a bug in the canonicalization process when we simplify /
fold declarations that contain `0<unit>` values.

The reason we even try to fold these in the first place is to simplify
values such as `m-[0rem]` to `m-0`. The more values we can
fold/canonicalize, the better we can suggest replacements _if_ they are
the same.

One thing we know in CSS is that if you have a `<length>` type, and that
value is `0<unit>`, then we can safely change that to just `0`.
```css
width: 0rem;
width: 0;    /* `0` is a <length> */
```
However, if this was part of a `calc(…)` (or another CSS math function),
then this could make the calc expression invalid:
- `calc(1rem + 0px)` → `calc(1rem + 0)` — this goes from _valid_ to
_invalid_
At runtime the `1rem` can be converted to a `px` based valued, then
`16px + 0px` makes sense. Adding `0` without unit does not.
- `calc(1rem * 0px)` → `calc(1rem * 0)` — this goes from _invalid_ to
_valid_
At runtime the `1rem` can be converted to a `px` based value, but `16px
* 0px` would result in `0px^2` which doesn't make sense either.

We will still normalize values such as `-0.0rem` to just `0rem`, but not
`0` if we know it's unsafe to do so.

If we end up with top-level `calc(…)` expressions that can be folded,
then we will try to do that:
- `calc(0px * -1)` → `0`
- `calc(calc(0px * -1) + 1rem)` → `calc(0px + 1rem)`
Notice that the inner `calc(…)` was folded to `0px` not `0` because that
would make the `calc(0 + 1rem)` invalid.
Additionally, we could potentially fold the `calc(0px + 1rem)` to just
`1rem`, but we have to make sure that we don't introduce valid values
from invalid values `calc(0s + 1rem)` would be invalid, but folding it
to `1rem` would make it valid which is not good.

Fixes:
https://github.com/tailwindlabs/tailwindcss-intellisense/issues/1579
2026-05-29 14:43:26 +02:00
Robin Malfait
829cdc930a
duration must be at least 500ms 2026-05-26 23:34:51 +02:00
Robin Malfait
2666194d5c
Improve internal instrumentation module (#20119)
This PR improves some of the internal instrumentation tooling we have.

While working on another branch, I updated the instrumentation tooling
to have a few different ways of measuring what's going on.

Until now, we had an `I.start(label)` and corresponding `I.end(label)`.
While this works, it also means that you have to make sure that you call
`I.end(label)` before every `return` to track things properly.

With this PR, I added a `I.span(label, () => /* some callback*/{})` API
that essentially does that in one go. It also handles promises and
resturns the value that was returned from the callback. This can be
useful in situations where you have a one-liner:
```ts
let css = I.span('toCss(…)', () => toCss(ast))
```

If your callback is longer, then you end up in a situation where you
have to indent your code, and if you want to stop measuring you have to
drop code in 2 places and re-indent:
```diff
- I.span('label', () => {
    …
- })
```

For this situation, I also added a `using _ = I.track(label)` API
instead. This can also be used in any block and automatically inserts
the `I.end(label)` on every exit point. This relies on the new `using`
keyword, but we already relied on that for the instrumentation module.

Last but not least, the constructor accepts a `shouldReport` which
defaults to the `env.DEBUG`. The reason for this change is so that it's
easier to report / not report during development instead of swapping out
an environment variable. Again, this is internal so there is no public
API change happening here.

## Test plan

All tests should still pass.
2026-05-26 23:20:39 +02:00
Robin Malfait
cdfae994ed
Remove unused test (#20117)
This PR cleans up some old stale test that has been skipped from the
beginning.

While this test wants to prove that migrating from Tailwind CSS v3 to
Tailwind CSS v4 can handle the `#{!important}` SCSS notation, it doesn't
prove that we can migrate an entire SCSS project. SCSS has much more
special syntax and we never supported that.

Let's get rid of this test that's doing nothing at the moment.
Especially since we don't want to migrate SCSS projects. Enabling this
might result in the false sense that we _do_ support SCSS migrations
which is not the case.

Closes: #20106
2026-05-26 19:45:00 +02:00
Robin Malfait
749c45ef26
Expose index and siblings on walk context (#20109)
This PR is a small improvement of the current `walk` implementation
where we will expose the `index` and the `siblings` on the current
context.

During a walk, we walk over objects that contain a `nodes: []` field.
The `ctx.parent` that already exists is a reference to the parent node,
but `ctx.siblings` is a reference to the `ctx.parent.nodes`.

The `ctx.index` is the index of the current node we are walking in the
`siblings` array. This way we can prevent the awkward
`ctx.parent?.nodes.indexOf(node)` which is a bit silly because we
already know the nodes we're walking and its index...

The `ctx.parent` can be `null`, but the `ctx.siblings` will never be
`null`, this can be seen in a situation like this:

```ts
let ast: AstNode = [nodeA, nodeB]

walk(ast, (node, ctx) => {
  if (node === nodeA) {
    ctx.parent === null; // Because there is no parent
    ctx.siblings === ast; // Because that's the current list we're looping over

    // Before this PR, we would have to do something like:
    let siblings = ctx.parent?.nodes ?? ast
  }
})
```

In the above example, the `ast` is a separately variable, but if this
was inlined, we would run into some issues:
```ts
walk([nodeA, nodeB], (node, ctx) => {
  if (node === nodeA) {
    ctx.parent === null; // Because there is no parent
    ctx.siblings === ast; // Because that's the current list we're looping over

    // At this point, there is no way to get to the `[nodeA, nodeB]` list
    // without moving it to a variable first.
  }
})
```

So, this PR doesn't change much, the additional information we track is
already known information that is now exposed to the caller of the
`walk` function.

In this PR we did update some usages and got rid of some awkward
`ctx.parent?.nodes ?? []` and `ctx.parent.nodes.indexOf(…)` usages.

## Test plan

- All tests still pass as expected
2026-05-24 22:32:05 +02:00
Jiahan Chen
982e920df8
Add Rspack optional peer dependency for @tailwindcss/webpack (#20027)
## Summary

This PR makes Rspack support explicit for `@tailwindcss/webpack` by
adding `@rspack/core` as an optional peer dependency alongside
`webpack`.

The loader already works through Rspack's webpack-compatible loader API,
as shown in this runnable example:
https://github.com/rstackjs/rstack-examples/tree/main/rspack/tailwindcss.
The README and package description are updated to document that usage.

## Test plan

No Rspack-specific tests were added. The loader implementation is
unchanged, and the existing webpack loader tests exercise the same
loader API path that Rspack uses, which should cover the relevant
behavior.

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2026-05-23 11:14:48 +02:00
Robin Malfait
73983e1cf5
Fix 'Sourcemap is likely to be incorrect' warnings when using @tailwindcss/vite (#20103)
This PR fixes an issue where a bunch of warnings would be shown related
to sourcemaps.

This happens when we are dealing with CSS files that are _not_ Tailwind
CSS roots. In that case, in the `transform` step, we return the `src` of
that module as-is because we didn't modify anything. However, when
nothing changed, you have to return a `NullValue` such as `undefined`.

So this is a stupid little fix, but it should get rid of a bunch of
annoying warnings.

Fixes: #19930

## Test plan

- Added an integration test to mimic the problem
- Other tests still pass
- Tested it against the reproduction provided in #19930


Before:
<img width="1887" height="1763" alt="8oQNG5Lqr2B"
src="https://github.com/user-attachments/assets/2d8af456-4176-4f18-92a3-5327e395ac6b"
/>


After:
<img width="1885" height="1404" alt="8oQND5kWSb4"
src="https://github.com/user-attachments/assets/7dd98ea4-126b-45d4-9411-0afbacd2c797"
/>
2026-05-22 17:57:25 +02:00
Robin Malfait
e0a0c46c85
Cleanup lockfile / dependencies (#20102)
This PR reduces the installed dependencies by cleaning up the
`pnpm-lock.yaml` file.

This also pins `@parcel/watcher` such that the lockfile is generated
properly becauase of the patched dependencies.

This is a follow-up of #19499, but up to date with the latest state of
the repo.

## Test plan

- Lockfile is simpler. Most dependencies stayed the same, and were
published _months_ ago. There are a few cases where we have more recent
published dependencies. There are 7 dependencies that were published in
the last ~24 hours: `node-releases@2.0.46` (10 hours ago),
`electron-to-chromium@1.5.361` (12 hours ago), `semver@7.8.1` (20 hours
ago), `terser@5.48.0` (20 hours ago), `webpack-sources@3.5.0` (5 hours
ago), `vite@8.0.14` (yesterday). All of these but the `terser` version
used OIDC.
- Socket.dev didn't report any issues with the changed dependencies
- All tests still pass

---------

Co-authored-by: James Garbutt <43081j@users.noreply.github.com>
2026-05-22 16:57:46 +02:00
Jordan Brough
a480043a8a
Add --silent option to suppress output in @tailwindcss/cli (#20100)
Edited by: @RobinMalfait 

This PR adds a new `--silent` option to the `@tailwindcss/cli` to
suppress output (except for errors).

## Test plan

- Existing tests pass
- A new integration test for the `--silent` option was added

---

Original:

## Summary

Small change to add a `--quiet` option.

@adamwathan previously said this type of thing [sounded like a good
idea](https://github.com/tailwindlabs/tailwindcss/issues/8050#issuecomment-1100164351):

> [I] have made a note about the --silent option idea which I think
definitely has value 👍🏻

This is helpful to keep logs high signal in some scenarios. For example,
when I want AI to sift through my foreman logs, where "Done in X"
becomes the dominant log message over time when running tailwind
alongside other things.

## Test plan

I've added an integration test.

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2026-05-22 13:03:32 +00:00