This PR adds a migration for migrating the changes we implemented in
https://github.com/tailwindlabs/tailwindcss/pull/14849
This is the migration we perform:
| Old | New |
| ----------------- | ------------------ |
| `shadow` | `shadow-sm` |
| `shadow-sm` | `shadow-xs` |
| `shadow-xs` | `shadow-2xs` |
| `inset-shadow` | `inset-shadow-sm` |
| `inset-shadow-sm` | `inset-shadow-xs` |
| `inset-shadow-xs` | `inset-shadow-2xs` |
| `drop-shadow` | `drop-shadow-sm` |
| `drop-shadow-sm` | `drop-shadow-xs` |
| `rounded` | `rounded-sm` |
| `rounded-sm` | `rounded-xs` |
| `blur` | `blur-sm` |
| `blur-sm` | `blur-xs` |
Also added an integration test to ensure that `shadow` is properly
migrated to `shadow-sm`, and doesn't get migrated to `shadow-xs`
(because `shadow-sm` is migrated to `shadow-xs`).
This PR removes all of the static `font-weight` utilities that were
previously hard-coded into the framework in favor of deriving those
utilities from the `--font-weight-*` theme values instead.
Biggest motivation for this is giving people a way to explicitly disable
font-weight utilities they don't want to use in their project, which
previously wasn't possible.
---------
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
This PR replaces the default spacing scale (`--spacing-*`) with a
generative system based on a default spacing _unit_.
Instead of the default theme containing values like `--spacing-4`,
`--spacing-6`, `--spacing-8`, etc., instead we just define a single
`--spacing` value:
```css
@theme {
--spacing: 0.25rem;
}
```
Utilities like `px-4` are derived from this unit by multiplying it by
the value in the utility (4 in this case):
```css
.px-4 {
padding-inline: calc(var(--spacing) * 4);
}
```
The biggest consequence of this change is that every value is available
now, rather than just the explicitly configured values.
This means utilities like `px-42` will work now, whereas prior to this
PR only `px-40` and `px-44` were valid utilities. I personally found it
very difficult to know which values actually existed at the higher end
of the scale without IntelliSense, and in practice even when working
with a skilled designer like [Steve](https://x.com/steveschoger) who
helped design Tailwind's default spacing scale, I'd very often need to
break out of it to implement a design, and trying to round to a value
that was in the scale made the design worse, not better.
This PR allows you to use any whole number, as well as decimal numbers
that are multiples of `0.25` to ensure classes like `px-1.5` continue to
work. While this means you can now technically do things like
`pt-97.25`, I think the presence of the fractional value will be enough
of a signal to developers that they are doing something a little
unusual, and they can use their judgment as to whether they are making
the right decision or not.
I'll update this PR with a lot more detail when I have a chance, as
there are a few other things to explain like:
- Unifying all of the values for
width/min-width/max-width/height/min-height/max-height utilities
- Deriving numeric line-height values from the spacing multiplier
instead of a separate line-height scale
- Using `--spacing: initial` to disable the multiplier
- How you can still use an explicit spacing scale and ignore this change
- How we plan to use IntelliSense to surface a more curated set of
spacing values even if smaller increments work when you type them
explicitly
---------
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
When migrating a project from Tailwind CSS v3 to Tailwind CSS v4, then
we started the migration process in the following order:
1. Migrate the JS/TS config file
2. Migrate the source files (found via the `content` option)
3. Migrate the CSS files
However, if you have a setup where you have multiple CSS root files
(e.g.: `frontend` and `admin` are separated), then that typically means
that you have an `@config` directive in your CSS files. These point to
the Tailwind CSS config file.
This PR changes the migration order to do the following:
1. Build a tree of all the CSS files
2. For each `@config` directive, migrate the JS/TS config file
3. For each JS/TS config file, migrate the source files
If a CSS file does not contain any `@config` directives, then we start
by filling in the `@config` directive with the default Tailwind CSS
config file (if found, or the one passed in). If no default config file
or passed in config file can be found, then we will error out (just like
we do now)
---------
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
This PR is an improvement/fix by making sure that whitespace around the
`,` separator is removed when printing arbitrary values.
Before:
```diff
- <div class="grid-cols-[min(50%,theme(spacing.80))_auto]"></div>
+ <div class="grid-cols-[min(50%,_var(--spacing-80))_auto]"></div>
```
After:
```diff
- <div class="grid-cols-[min(50%,theme(spacing.80))_auto]"></div>
+ <div class="grid-cols-[min(50%,var(--spacing-80))_auto]"></div>
```
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
This PR fixes an issue where a `@source` crashes when the path
eventually resolves to a path ending in `..`.
We have to make sure that we canonicalize the path to make sure that we
are working with the real directory.
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
This PR improves the PostCSS migrations to make sure that we install
`@tailwindcss/postcss` in the same bucket as `tailwindcss`.
If `tailwindcss` exists in the `dependencies` bucket, we install
`@tailwindcss/postcss` in the same bucket. If `tailwindcss` exists in
the `devDependencies` bucket, we install `@tailwindcss/postcss` in the
same bucket.
This also contains an internal refactor that normalizes the package
manager to make sure we can install a package to the correct bucket
depending on the package manager.
---------
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
This PR introduces a new `source(…)` argument and improves on the
existing `@source`. The goal of this PR is to make the automatic source
detection configurable, let's dig in.
By default, we will perform automatic source detection starting at the
current working directory. Auto source detection will find plain text
files (no binaries, images, ...) and will ignore git-ignored files.
If you want to start from a different directory, you can use the new
`source(…)` next to the `@import "tailwindcss/utilities"
layer(utilities) source(…)`.
E.g.:
```css
/* ./src/styles/index.css */
@import 'tailwindcss/utilities' layer(utilities) source('../../');
```
Most people won't split their source files, and will just use the simple
`@import "tailwindcss";`, because of this reason, you can use
`source(…)` on the import as well:
E.g.:
```css
/* ./src/styles/index.css */
@import 'tailwindcss' source('../../');
```
Sometimes, you want to rely on auto source detection, but also want to
look in another directory for source files. In this case, yuo can use
the `@source` directive:
```css
/* ./src/index.css */
@import 'tailwindcss';
/* Look for `blade.php` files in `../resources/views` */
@source '../resources/views/**/*.blade.php';
```
However, you don't need to specify the extension, instead you can just
point the directory and all the same automatic source detection rules
will apply.
```css
/* ./src/index.css */
@import 'tailwindcss';
@source '../resources/views';
```
If, for whatever reason, you want to disable the default source
detection feature entirely, and only want to rely on very specific glob
patterns you define, then you can disable it via `source(none)`.
```css
/* Completely disable the default auto source detection */
@import 'tailwindcss' source(none);
/* Only look at .blade.php files, nothing else */
@source "../resources/views/**/*.blade.php";
```
Note: even with `source(none)`, if your `@source` points to a directory,
then auto source detection will still be performed in that directory. If
you don't want that, then you can simply add explicit files in the globs
as seen in the previous example.
```css
/* Completely disable the default auto source detection */
@import 'tailwindcss' source(none);
/* Run auto source detection in `../resources/views` */
@source "../resources/views";
```
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
This PR introduces an internal refactor where we introduce the `AtRule`
CSS Node in our AST.
The motivation for this is that in a lot of places we need to
differentiate between a `Rule` and an `AtRule`. We often do this with
code that looks like this:
```ts
rule.selector[0] === '@' && rule.selector.startsWith('@media')
```
Another issue we have is that we often need to check for `'@media '`
including the space, because we don't want to match `@mediafoobar` if
somebody has this in their CSS. Alternatively, if you CSS is minified
then it could be that you have a rule that looks like
`@media(width>=100px)`, in this case we _also_ have to check for
`@media(`.
Here is a snippet of code that we have in our codebase:
```ts
// Find at-rules rules
if (node.kind === 'rule') {
if (
node.selector[0] === '@' &&
(node.selector.startsWith('@media ') ||
node.selector.startsWith('@media(') ||
node.selector.startsWith('@custom-media ') ||
node.selector.startsWith('@custom-media(') ||
node.selector.startsWith('@container ') ||
node.selector.startsWith('@container(') ||
node.selector.startsWith('@supports ') ||
node.selector.startsWith('@supports(')) &&
node.selector.includes(THEME_FUNCTION_INVOCATION)
) {
node.selector = substituteFunctionsInValue(node.selector, resolveThemeValue)
}
}
```
Which will now be replaced with a much simpler version:
```ts
// Find at-rules rules
if (node.kind === 'at-rule') {
if (
(node.name === '@media' ||
node.name === '@custom-media' ||
node.name === '@container' ||
node.name === '@supports') &&
node.params.includes(THEME_FUNCTION_INVOCATION)
) {
node.params = substituteFunctionsInValue(node.params, resolveThemeValue)
}
}
```
Checking for all the cases from the first snippet is not the end of the
world, but it is error prone. It's easy to miss a case.
A direct comparison is also faster than comparing via the
`startsWith(…)` function.
---
Note: this is only a refactor without changing other code _unless_ it
was required to make the tests pass. The tests themselves are all
passing and none of them changed (because the behavior should be the
same).
The one exception is the tests where we check the parsed AST, which now
includes `at-rule` nodes instead of `rule` nodes when we have an
at-rule.
This PR adds a migration to bump the `prettier-plugin-tailwindcss`
version to the latest version when upgrading your project. This is to
ensure that the plugin is compatible with the latest version of Tailwind
CSS.
Note: we will only do this _if_ you already used the
`prettier-plugin-tailwindcss` plugin in your project.
This PR is a continuation of #14783 to handle the feedback on that PR.
1. Update the test to be more realistic
2. Updated the comment
---------
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
This PR fixes an issue where `layer(…)` next to imports were removed
where they shouldn't have been removed.
The issue exists if _any_ of the `@import` nodes in a file contains
`@utility`, if that's the case then we removed the `layer(…)` next to
_all_ `@import` nodes.
Before we were checking if the current sheet contained `@utility` or in
any of its children (sub-`@import` nodes).
This fixes that by looping over the `@import` nodes in the current
sheet, and looking for the `@utility` in the associated/imported file.
This way we update each node individually.
Test plan:
---
Added a dedicated integration test to make sure all codemods together
result in the correct result. Input:
96e8908378/integrations/upgrade/index.test.ts (L2076-L2108)
Output:
96e8908378/integrations/upgrade/index.test.ts (L2116-L2126)
This PR improves where we inject the border compatibility CSS. Before
this change we injected it if it was necessary in one of these spots:
- Above the first `@layer base` to group it together with existing
`@layer base` at-rules.
- If not present, after the last `@import`, to make sure that we emit
valid CSS because `@import` should be at the top (with a few
exceptions).
However, if you are working with multiple CSS files, then it could be
that we injected the border compatibility CSS multiple times if those
files met one of the above conditions.
To solve this, we now inject the border compatibility CSS with the same
rules as above, but we also have another condition:
The border compatibility CSS is only injected if the file also has a
`@import "tailwindcss";` _or_ `@import "tailwindcss/preflight";` in the
current file.
---
Added integration tests to make sure that we are generating what we
expect in a real environment. Some of the integration tests also use the
old `@tailwind` directives to make sure that the order of migrations is
correct (first migrate to `@import` syntax, then inject the border
compatibility CSS).
---------
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
This PR fixes an issue where currently a `theme()` function call inside
an arbitrary value that used a dot in the key path:
```jsx
let className = "ml-[theme(spacing[1.5])]"
```
Was causing issues when going though the codemod. The issue is that for
candidates, we require `_` to be _escaped_, since otherwise they will be
replaced with underscore. When going through the codemods, the above
candidate would be translated to the following CSS variable access:
```js
let className = "ml-[var(--spacing-1\_5))"
```
Because the underscore was escaped, we now have an invalid string inside
a JavaScript file (as the `\` would escape inside the quoted string.
To resolve this, we decided that this common case (as its used by the
Tailwind CSS default theme) should work without escaping. In
https://github.com/tailwindlabs/tailwindcss/pull/14776, we made the
changes that CSS variables used via `var()` no longer unescape
underscores. This PR extends that so that the Variant printer (that
creates the serialized candidate representation after the codemods make
changes) take this new encoding into account.
This will result in the above example being translated into:
```js
let className = "ml-[var(--spacing-1_5))"
```
With no more escaping. Nice!
## Test Plan
I have added test for this to the kitchen-sink upgrade tests.
Furthermore, to ensure this really works full-stack, I have updated the
kitchen-sink test to _actually build the migrated project with Tailwind
CSS v4_. After doing so, we can assert that we indeed have the right
class name in the generated CSS.
---------
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
This PR improves the heuristics around the important codemod (e.g.
`!border` => `border!`) as we noticed a few more cases where we the
current heuristics was not enough.
Specifically, we made it not migrate the candidate in the following
conditions:
- When there's an immediate property access: `{ "foo": !border.something
+ ""}`
- When it's used as condition in the template language: `<div
v-if="something && !border"></div>` or `<div x-if="!border"></div>`
## Test plan
I added test cases to the unit tests and updated the integration test to
contain a more sophisticated example.
---------
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
This PR makes sure the `migrateImport` codemod is properly registered so
that it runs as part of the upgrade process.
## Test plan
This PR adds a new `v3` playground with an `upgrade` script that you can
use to run the upgrade from the local package. When you add a
non-prefixed `@import` to the v3 example, the paths are now properly
updated with no errors logged:
https://github.com/user-attachments/assets/85949bbb-756b-4ee2-8ac0-234fe1b2ca39
---------
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
Co-authored-by: Philipp Spiess <hello@philippspiess.com>
This PR fixes an issue where JS configuration theme properties with dots
or slashes in them would not migrate correctly. E.g.:
```ts
import { type Config } from 'tailwindcss'
module.exports = {
theme: {
width: {
1.5: '0.375rem',
'1/2': '50%',
}
}
}
```
This should convert to:
```css
@theme {
--width-1_5: 0.375rem;
--width-1\/2: 50%;
}
```
_Note: We will likely change the `--width-1_5` key to `--width-1\.5` in
a follow-up PR._
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
When we implemented the CSS import resolution system, we found out a
detail about CSS imports in that files without a relative path prefix
would still be relative to the source file. E.g.:
```css
@import 'foo.css';
```
Should first look for the file `foo.css` in the same directory. To make
this cost as cheap as possible, we limited this by a heuristics to only
apply the auto-relative imports for files with a file extension.
Naturally, while testing v4 on more templates, we found that it's common
for people to omit the file extension when loading css file. The above
could also be written as such:
```css
@import 'foo';
```
To improve this, we have two options:
- We either remove the heuristics, making every `@import` more expensive
because we have to check for relative files.
- We upgrade our codemods to rewrite `@import` statements to be
explicitly relative.
Because we really care about performance, we opted to go with the latter
option. This PR adds the codemod and removes the heuristics so we
resolve CSS files similar to how you would resolve JS files.
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
This PR adds a codemod that ensures that the border styles from Tailwind CSS v3 work as expected once your project is migrated to Tailwind CSS v4.
In Tailwind CSS v3, the default border color is `colors.gray.200` and in Tailwind CSS v4 the default border color is `currentColor`.
Similarly in Tailwind CSS v3, DOM elements such as `input`, `select`, and `textarea` have a border width of `0px`, in Tailwind CSS v4, we don't change the border width of these elements and keep them as `1px`.
If your project happens to already use the same value for the default border color (`currentColor`) as we use in Tailwind CSS v4, then nothing happens. But this is very unlikely, so we will make sure that we honor your `borderColor.DEFAULT` value.
If you didn't change the default values in your `tailwind.config.js`, then we will inject compatibility CSS using the default Tailwind CSS v3 values to ensure the default color and width are applied correctly.
The important candidate migration is one of the most broad we have since it matches for any utility that are prefixed with an exclamation mark.
When running the codemodes on our example projects, we noticed that this was instead creating false-positives with candidates used in code positions, e.g:
```ts
export default {
shouldNotUse: !border.shouldUse,
}
```
To prevent false-positives, this PR adds a heuristics to detect wether or not a candidate is used in a non-code position. We do this by checking the character before and after the modifier and only allow quotes or spaces.
This can cause candidates to not migrate that are valid Tailwind CSS classes, e.g.:
```ts
let classNames = `!underline${isHovered ? ' font-bold' : ''}`
```
This, however, is not a big issue since v4 can parse the v3 important prefix too.
This PR adds a codemod for migrating the old `@screen` directive from Tailwind
CSS v2 that also worked in Tailwind CSS v3 but wasn't documented anymore.
Internally, this first migrates `@screen md` to `@media screen(md)`, then we rely on the existing migration that migrates the `screen(…)` function.
Input:
```css
@screen md {
.foo {
color: red;
}
}
```
Output (IR):
```css
@media screen(md) {
.foo {
color: red;
}
}
```
Output:
```css
@media theme(--breakpoint-md) {
.foo {
color: red;
}
}
```
This PR migrates the `@variants` and `@responsive` directives.
In Tailwind CSS v2, these were used to generate certain variants of responsive variants for the give classes. In Tailwind CSS v3, these still worked but were implemented as a no-op such that these directives don't end up in your final CSS.
In Tailwind CSS v4, these don't exist at all anymore, so we can safely get rid of them by replacing them with their contents.
Input:
```css
@variants hover, focus {
.foo {
color: red;
}
}
@responsive {
.bar {
color: blue;
}
}
```
Output:
```css
.foo {
color: red;
}
.bar {
color: blue;
}
```
We have a migration that adds the `layer(…)` next to the `@import`
depending on the order of original values. For example:
```css
@import "tailwindcss/utilities":
@import "./foo.css":
@import "tailwindcss/components":
```
Will be turned into:
```css
@import "tailwindcss":
@import "./foo.css" layer(utilities):
```
Because it used to exist between `utilities` and `components`. Without
this it would be _after_ `components`.
This results in an issue if an import has (deeply) nested `@utility`
at-rules after migrations. This is because if this is generated:
```css
/* ./src/index.css */
@import "tailwindcss";
@import "./foo.css" layer(utilities);
/* ./src/foo.css */
@utility foo {
color: red;
}
```
Once we interpret this (and thus flatten it), the final CSS would look
like:
```css
@layer utilities {
@utility foo {
color: red;
}
}
```
This means that `@utility` is not top-level and an error would occur.
This fixes that by removing the `layer(…)` from the import if the
imported file (or any of its children) contains an `@utility`. This is
to ensure that once everything is imported and flattened, that all
`@utility` at-rules are top-level.
This PR enables JS configuration files with `corePlugins` themes to be
migrated. If such option is found in your config, we will warn the user
and omit the option from the resulting CSS file as there is no v4
alternative.
---------
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
This PR fixes an issue where `theme(…)` calls that contain a `.1`
weren't correctly converted to `var(--spacing-1)`. The reason for this
is that `.1` has some special meaning in cases like
`fontSize.xs.1.lineHeight` where it should be converted to
`--font-size-xs--line-height`, not `--font-size-xs-1-line-height`.
To solve this, we make sure to only apply the `--` check if the `1`
occurs somewhere in the middle instead of at the very end.
With this change, the following migrations will happen correctly:
```diff
- [--value:theme(spacing.1)]
+ [--value:var(--spacing-1)]
```
```diff
- [--value:theme(fontSize.xs.1.lineHeight)]
+ [--value:var(--font-size-xs--line-height)]
```
---------
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
This PR will optimize and simplify the candidates when printing the
candidate again after running codemods.
When we parse a candidate, we will add spaces around operators, for
example `p-[calc(1px+1px)]]` will internally be handled as `calc(1px +
1px)`. Before this change, we would re-print this as:
`p-[calc(1px_+_1px)]`.
This PR changes that by simplifying the candidate again so that the
output is `p-[calc(1px+1px)]`. In addition, if _you_ wrote
`p-[calc(1px_+_1px)]` then we will also simplify it to the concise form
`p-[calc(1px_+_1px)]`.
Some examples:
Input:
```html
<div class="[p]:flex"></div>
<div class="[&:is(p)]:flex"></div>
<div class="has-[p]:flex"></div>
<div class="px-[theme(spacing.4)-1px]"></div>
```
Output before:
```html
<div class="[&:is(p)]:flex"></div>
<div class="[&:is(p)]:flex"></div>
<div class="has-[&:is(p)]:flex"></div>
<div class="px-[var(--spacing-4)_-_1px]"></div>
```
Output after:
```html
<div class="[p]:flex"></div>
<div class="[p]:flex"></div>
<div class="has-[p]:flex"></div>
<div class="px-[var(--spacing-4)-1px]"></div>
```
---
This is alternative implementation to #14717 and #14718Closes: #14717Closes: #14718
This PR adds missing legacy migrations for migrating `flex-grow` to
`grow` and `flex-shrink` to `shrink`.
We already migrated `flex-grow-0` to `grow-0` and `flex-shrink-0` to
`shrink-0`, but forgot about these cases.
This PR changes the migration of `content` rules in the JS config to CSS codemods.
When a `content` rule is processed which matches files that are _also matched by the automatic content discovery in v4_, we do not need to emit CSS for that rule.
Take, for example this v3 configuration file:
```ts
import { type Config } from 'tailwindcss'
module.exports = {
content: [
'./src/**/*.{html,js}',
'./node_modules/my-external-lib/**/*.{html}'
],
} satisfies Config
```
Provided the base directories match up, the first rule will also be covered by the automatic content discovery in v4 and thus we only need to convert the second rule to CSS:
```css
@import "tailwindcss";
@source '../node_modules/my-external-lib/**/*.{html}';
```
This PR extends our JS configuration to CSS migration by also allowing `plugins` with options.
An example of such config would be:
```js
import { type Config } from 'tailwindcss'
import myPlugin from "./myPlugin";
export default {
plugins: [
myPlugin({
class: "tw",
}),
],
} satisfies Config;
```
If the option object contains only values allowed in our CSS API, we can convert this to CSS entirely:
```css
@plugin './myPlugin' {
class: 'tw';
}
```
This PR updates the `extractStaticPlugins` function to also emit options as long as these are objects containing of only `string` and `number` values.
While doing this I also cleaned up the `require('custom-plugin')` detector to use a Tree-Sitter query instead of operating on the AST.
Here are the two cases we considered:
```js
import plugin1 from 'plugin1';
export default {
plugins: [
plugin1({
foo: 'bar',
num: 19,
}),
require('./plugin2')({
foo: 'bar',
num: 19,
}),
]
}
```
The test plan also contains a number of scenarios that we do not want to migrate to CSS (because we do not have a CSS API we can use for e.g. nested objects). We do support all types that we also support in the CSS API.
With the changes in #14672, it now becomes trivial to actually resolve
the config (while still retaining the reset behavior). This means that
we can now convert JS configs that use _functions_, e.g.:
```ts
import { type Config } from 'tailwindcss'
export default {
theme: {
extend: {
colors: ({ colors }) => ({
gray: colors.neutral,
}),
},
},
} satisfies Config
```
This becomes:
```css
@import 'tailwindcss';
@theme {
--color-gray-50: #fafafa;
--color-gray-100: #f5f5f5;
--color-gray-200: #e5e5e5;
--color-gray-300: #d4d4d4;
--color-gray-400: #a3a3a3;
--color-gray-500: #737373;
--color-gray-600: #525252;
--color-gray-700: #404040;
--color-gray-800: #262626;
--color-gray-900: #171717;
--color-gray-950: #0a0a0a;
}
```
---------
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This PR is a follow up from https://github.com/tailwindlabs/tailwindcss/pull/14664 migrates all the `theme(…)` calls in your CSS to `var(…)` if we can.
In at-rules, like `@media` we can't use `var(…)` so we have to use the modern version of `theme(…)`.
In declarations, we can convert to `var(…)` unless there is a modifier used in the `theme(…)` function, then we can only convert to the new `theme(…)` syntax without dot notation.
Input:
```css
@media theme(spacing.4) {
.foo {
background-color: theme(colors.red.900);
color: theme(colors.red.900 / 75%); /* With spaces around the `/` */
border-color: theme(colors.red.200/75%); /* Without spaces around the `/` */
}
}
```
Output:
```css
@media theme(--spacing-4) {
/* ^^^^^^^^^^^^^^^^^^ Use `theme(…)` since `var(…)` is invalid in this position*/
.foo {
background-color: var(--color-red-900); /* Converted to var(…) */
color: theme(--color-red-900 / 75%); /* Convert to modern theme(…) */
border-color: theme(--color-red-200 / 75%); /* Convert to modern theme(…) — pretty printed*/
}
}
```
This PR makes the `themeToVar` code we already use for migrating the `theme(…)` calls to `var(…)` calls in the candidates (found in source files), reusable for the next PR where we want to migrate `theme(…)` calls in your actual CSS.
(This shouldn't really require a separate PR, but I'm playing with Graphite's stacked PR feature to see how it works in practice 😅)
This PR adds a codemod to convert `theme(…)` calls to `var(…)` calls. If
we can't safely do this, then we try to convert the `theme(…)` syntax
(dot notation) to the modern `theme(…)` syntax (with CSS variable-like
syntax).
### Let's look at some examples:
**Simple example:**
Input:
```html
<div class="bg-[theme(colors.red.500)]"></div>
```
Output:
```html
<div class="bg-[var(--color-red-500)]"></div>
```
---
**With fallback:**
Input:
```html
<div class="bg-[theme(colors.red.500,theme(colors.blue.500))]"></div>
```
Output:
```html
<div class="bg-[var(--color-red-500,var(--color-blue-500))]"></div>
```
---
**With modifiers:**
Input:
```html
<div class="bg-[theme(colors.red.500/75%)]"></div>
```
Output:
```html
<div class="bg-[var(--color-red-500)]/75"></div>
```
We can special case this, because if you are using that modifier syntax
we _assume_ it's being used in a `theme(…)` call referencing a color.
This means that we can also convert it to a modifier on the actual
candidate.
---
**With modifier, if a modifier is already present:**
Input:
```html
<div class="bg-[theme(colors.red.500/75%)]/50"></div>
```
Output:
```html
<div class="bg-[theme(--color-red-500/75%)]/50"></div>
```
In this case we can't use the `var(…)` syntax because that requires us
to move the opacity modifier to the candidate itself. In this case we
could use math to figure out the expected modifier, but that might be
too confusing. Instead, we convert to the modern `theme(…)` syntax.
---
**Multiple `theme(…)` calls with modifiers:**
Input:
```html
<div class="bg-[theme(colors.red.500/75%,theme(colors.blue.500/50%))]"></div>
```
Output:
```html
<div class="bg-[theme(--color-red-500/75%,theme(--color-blue-500/50%))]"></div>
```
In this case we can't convert to `var(…)` syntax because then we lose
the opacity modifier. We also can't move the opacity modifier to the
candidate itself e.g.: `/50` because we have 2 different variables to
worry about.
In this situation we convert to the modern `theme(…)` syntax itself.
---
**Inside variants:**
Input:
```html
<div class="max-[theme(spacing.20)]:flex"></div>
```
Output:
```html
<div class="max-[theme(--spacing-20)]:flex"></div>
```
Unfortunately we can't convert to `var(…)` syntax reliably because in
some cases (like the one above) the value will be used inside of an
`@media (…)` query and CSS doesn't support that at the time of writing
this PR.
So to be safe, we will not try to convert `theme(…)` to `var(…)` in
variants, but we will only upgrade the `theme(…)` call itself to modern
syntax.
Imagine the following setup:
```css
/* src/input.css */
@import "tailwindcss";
@config "../tailwind.config.ts";
@theme {
--color-red-500: #ef4444;
}
```
```ts
/* tailwind.config.ts */
export default {
theme: {
colors: {
red: {
600: '#dc2626'
}
},
extend: {
colors: {
400: '#f87171'
}
}
}
}
```
Since the theme object in the JS config contains `colors` in the
non-`extends` block, you would expect this to _not pull in all the
default colors imported via `@import "tailwindcss";`_. This, however,
wasn't the case right now since all theme options were purely _additive_
to the CSS.
This PR makes it so that non-`extend` theme keys _overwrite default CSS
theme values_. The emphasis is on `default` here since you still want to
be able to overwrite your options via `@theme {}` in user space.
This now generates the same CSS that our upgrade codemods would also
generate as this would apply the new CSS right after the `@import
"tailwindcss";` rule resulting in:
```css
@import "tailwindcss";
@theme {
--color-*: initial;
--color-red-400: #f87171;
--color-red-600: #dc2626;
}
@theme {
--color-red-500: #ef4444;
}
```
## Keyframes
This PR also adds a new core API to unset keyframes the same way. We
previously had no option of doing that but while working on the above
codemods we noticed that keyframes should behave the same way:
```css
@import "tailwindcss";
@theme {
--keyframes-*: initial;
@keyframes spin {
to {
transform: rotate(361deg);
}
}
}
```
To do this, the keyframes bookeeping was moved from the main Tailwind
CSS v4 file into the `Theme` class.
_I’m not sure super of the API yet but we would need a way for the
codemods to behave the same as out interop layer here. Option B is that
we don't reset keyframes the same way we reset other theme variables_.
This PR does two things:
- Computes UTF-16 string positions in Rust rather than in JS —
eliminating a significant number of traversals of the input string
- Applies replacements to the content in ascending order so we only ever
move forward through the source string — this lets v8 optimize string
concatenation
This PR adds a codemod that can convert arbitrary values to the cleaner
bare values if we can.
For example, some classes use arbitrary values such as `col-start-[16]`,
but in v4 we have bare values for some plugins that don't really need to
adhere to your design system.
In this case, we can convert `col-start-[16]` to just `col-start-16`.
Another use case is for utilities that use fractions. For example the
`aspect-*` plugin.
A custom aspect ratio such as `aspect-[16/9]` can be converted to
`aspect-16/9`.
There are some rules attached to this migration:
1. We can only migrate arbitrary values that is a single positive
integer, or an arbitrary value that is a fraction where the numerator
and denominator are both positive integers.
2. We make sure that some CSS can be generated once its converted to a
bare value.
This PR builds on top of the new [JS config to CSS
migration](https://github.com/tailwindlabs/tailwindcss/pull/14651) and
extends it to support migrating _static_ plugins.
What are _static_ plugins you might ask? Static plugins are plugins
where we can statically determine that these are coming from a different
file (so there is nothing inside the JS config that creates them). An
example for this is this config file:
```js
import typographyPlugin from '@tailwindcss/typography'
import { type Config } from 'tailwindcss'
export default {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
darkMode: 'selector',
plugins: [typographyPlugin],
} satisfies Config
```
Here, the `plugins` array only has one element and it is a static import
from the `@tailwindcss/typography` module. In this PR we attempt to
parse the config file via Tree-sitter to extract the following
information from this file:
- What are the contents of the `plugins` array
- What are statically imported resources from the file
We then check if _all_ entries in the `plugins` array are either static
resources or _strings_ (something I saw working in some tests but I’m
not sure it still does). We migrate the JS config file to CSS if all
plugins are static and we can migrate them to CSS `@plugin` calls.
## Todo
This will need to be rebased after the updated tests in #14648
This PR adds a new codemod that can migrate `data-*` and `aria-*`
variants using arbitrary values to bare values.
In Tailwind CSS v3, if you want to conditionally apply a class using
data attributes, then you can write `data-[selected]:flex`. This
requires the DOM element to have a `data-selected=""` attribute. In
Tailwind CSS v4 we can simplify this, by dropping the brackets and by
using `data-selected:flex` directly.
This migration operates on the internal AST, which means that this also
just works for compound variants such as
`group-has-data-[selected]:flex` (which turns into
`group-has-data-selected:flex`).
Additionally, this codemod is also applicable to `aria-*` variants. The
biggest difference is that in v4 `aria-selected` maps to an attribute of
`aria-selected="true"`. This means that we can only migrate
`aria=[selected="true"]:flex` to `aria-selected:flex`.
Last but not least, we also migrate `supports-[gap]` to `supports-gap`
if the passed in value looks like a property. If not, e.g.:
`supports-[display:grid]` then it stays as-is.
This PR implements the first version of JS config file migration to CSS.
It is based on the most simple config setups we are using in the
Tailwind UI templates Commit, Primer, Radiant, and Studio.
The example we use in the integration test is a config that looks like
this:
```js
import { type Config } from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'
module.exports = {
darkMode: 'selector',
content: ['./src/**/*.{html,js}'],
theme: {
boxShadow: {
sm: '0 2px 6px rgb(15 23 42 / 0.08)',
},
colors: {
red: {
500: '#ef4444',
},
},
fontSize: {
xs: ['0.75rem', { lineHeight: '1rem' }],
sm: ['0.875rem', { lineHeight: '1.5rem' }],
base: ['1rem', { lineHeight: '2rem' }],
},
extend: {
colors: {
red: {
600: '#dc2626',
},
},
fontFamily: {
sans: 'Inter, system-ui, sans-serif',
display: ['Cabinet Grotesk', ...defaultTheme.fontFamily.sans],
},
borderRadius: {
'4xl': '2rem',
},
},
},
plugins: [],
} satisfies Config
```
As you can see, this file only has a `darkMode` selector, custom
`content` globs, a `theme` (with some theme keys being overwriting the
default theme and some others extending the defaults). Note that it does
not support `plugins` and/or `presets` yet.
In the case above, we will find the CSS file containing the existing
`@tailwind` directives and are migrating it to the following:
```css
@import 'tailwindcss';
@source './**/*.{html,js}';
@variant dark (&:where(.dark, .dark *));
@theme {
--box-shadow-*: initial;
--box-shadow-sm: 0 2px 6px rgb(15 23 42 / 0.08);
--color-*: initial;
--color-red-500: #ef4444;
--font-size-*: initial;
--font-size-xs: 0.75rem;
--font-size-xs--line-height: 1rem;
--font-size-sm: 0.875rem;
--font-size-sm--line-height: 1.5rem;
--font-size-base: 1rem;
--font-size-base--line-height: 2rem;
--color-red-600: #dc2626;
--font-family-sans: Inter, system-ui, sans-serif;
--font-family-display: Cabinet Grotesk, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--border-radius-4xl: 2rem;
}
```
This replicates all features of the JS config so we can even delete the
existing JS config in this case.
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This PR adds a mapping from legacy classes to new classes. For example,
the `flex-shrink-0` is still used in our projects, but is deprecated in
v3.
The migration does a tiny bit of parsing because we can't rely on
`designSystem.parseCandidate(…)` because this requires the utility to be
defined which is not the case for legacy classes.
This migration runs _after_ the migration where we handle prefixes, so
we don't have to worry about that. We do have to worry about the `!`
location, because the `important` migration also relies on the
`designSystem`.
| Old | New |
| ------------------- | ---------------------- |
| `overflow-clip` | `text-clip` |
| `overflow-ellipsis` | `text-ellipsis` |
| `flex-grow-0` | `grow-0` |
| `flex-shrink-0` | `shrink-0` |
| `decoration-clone` | `box-decoration-clone` |
| `decoration-slice` | `box-decoration-slice` |
This PR injects a `@config "…"` in the CSS file if a JS based config has
been found.
We will try to inject the `@config` in a sensible place:
1. Above the very first `@theme`
2. If that doesn't work, below the last `@import`
3. If that doesn't work, at the top of the file (as a last resort)
When a stylesheet is imported with `@import “…” layer(utilities)` that
means that all classes in that stylesheet and any of its imported
stylesheets become candidates for `@utility` conversion.
Doing this correctly requires us to place `@utility` rules into separate
stylesheets (usually) and replicate the import tree without layers as
`@utility` MUST be root-level. If a file consists of only utilities we
won't create a separate file for it and instead place the `@utility`
rules in the same stylesheet.
Been doing a LOT of pairing with @RobinMalfait on this one but I think
this is finally ready to be looked at
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>