When CSS exists between two tailwind directives, then the CSS will be
wrapped in an `@layer` to ensure it all ends up in the correct location
in the final CSS file.
However, if the only thing between two tailwind directives is a comment,
then the comment will also be wrapped in an `@layer` directive.
E.g.:
```css
@tailwind base;
/* This is a comment */
@tailwind components;
/* This is another comment */
@tailwind utilities;
```
Results in:
```css
@import "tailwindcss";
@layer base {
/* This is a comment */
}
@layer components {
/* This is another comment */
}
```
In this case, the layers don't matter, so this can be simplified to:
```css
@import "tailwindcss";
/* This is a comment */
/* This is another comment */
```
This PR fixes an issue where some `@import` at-rules had an empty
`layer()` attached at the end of the `@import` string.
We should only add that if a Tailwind directive or Tailwind import such
as `@tailwind base` or `@import "tailwindcss/base"` preceded the current
`@import` at-rule. If there was no Tailwind directive, the `lastLayer`
will be empty and we don't need to attach it to the `@import` string.
This PR improves the missing layers codemod where everything after the
last Tailwind directive can stay as-is without wrapping it in a `@layer`
directive.
The `@layer` at-rules are only important for CSS that exists between
Tailwind directives.
E.g.:
```css
@tailwind base;
html {}
@tailwind components;
.btn {}
@tailwind utilities;
.foo {}
.bar {}
```
Was transformed into:
```css
@import "tailwindcss";
@layer base {
html {}
}
@layer components {
.btn {}
}
@layer utilities {
.foo {}
.bar {}
}
```
But the last `@layer utilities` is already in the correct spot, so we
can simplify this to just this instead:
```css
@import "tailwindcss";
@layer base {
html {}
}
@layer components {
.btn {}
}
.foo {}
.bar {}
```
This PR adds a codemod that ensures that some parts of your stylesheet
are wrapped in an `@layer`.
This is a follow-up PR of #14411, in that PR we migrate `@tailwind`
directives to imports.
As a quick summary, that will turn this:
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```
Into:
```css
@import 'tailwindcss';
```
But there are a few issues with that _if_ we have additional CSS on the
page. For example let's imagine we had this:
```css
@tailwind base;
body {
background-color: red;
}
@tailwind components;
.btn {}
@tailwind utilities;
```
This will now be turned into:
```css
@import 'tailwindcss';
body {
background-color: red;
}
.btn {}
```
But in v4 we use real layers, in v3 we used to replace the directive
with the result of that layer. This means that now the `body` and `.btn`
styles are in the incorrect spot.
To solve this, we have to wrap them in a layer. The `body` should go in
an `@layer base`, and the `.btn` should be in an `@layer components` to
make sure it's in the same spot as it was before.
That's what this PR does, the original input will now be turned into:
```css
@import 'tailwindcss';
@layer base {
body {
background-color: red;
}
}
@layer components {
.btn {
}
}
```
There are a few internal refactors going on as well, but those are less
important.
This PR adds CSS codemods for migrating existing `@layer utilities` to
`@utility` directives.
This PR has the ability to migrate the following cases:
---
The most basic case is when you want to migrate a simple class to a
utility directive.
Input:
```css
@layer utilities {
.foo {
color: red;
}
.bar {
color: blue;
}
}
```
Output:
```css
@utility foo {
color: red;
}
@utility bar {
color: blue;
}
```
You'll notice that the class `foo` will be used as the utility name, the
declarations (and the rest of the body of the rule) will become the body
of the `@utility` definition.
---
In v3, every class in a selector will become a utility. To correctly
migrate this to `@utility` directives, we have to register each class in
the selector and generate `n` utilities.
We can use nesting syntax, and replace the current class with `&` to
ensure that the final result behaves the same.
Input:
```css
@layer utilities {
.foo .bar .baz {
color: red;
}
}
```
Output:
```css
@utility foo {
& .bar .baz {
color: red;
}
}
@utility bar {
.foo & .baz {
color: red;
}
}
@utility .baz {
.foo .bar & {
color: red;
}
}
```
In this case, it could be that you know that some of them will never be
used as a utility (e.g.: `hover:bar`), but then you can safely remove
them.
---
Even classes inside of `:has(…)` will become a utility. The only
exception to the rule is that we don't do it for `:not(…)`.
Input:
```css
@layer utilities {
.foo .bar:not(.qux):has(.baz) {
display: none;
}
}
```
Output:
```css
@utility foo {
& .bar:not(.qux):has(.baz) {
display: none;
}
}
@utility bar {
.foo &:not(.qux):has(.baz) {
display: none;
}
}
@utility baz {
.foo .bar:not(.qux):has(&) {
display: none;
}
}
```
Notice that there is no `@utility qux` because it was used inside of
`:not(…)`.
---
When classes are nested inside at-rules, then these classes will also
become utilities. However, the `@utility <name>` will be at the top and
the at-rules will live inside of it. If there are multiple classes
inside a shared at-rule, then the at-rule will be duplicated for each
class.
Let's look at an example to make it more clear:
Input:
```css
@layer utilities {
@media (min-width: 640px) {
.foo {
color: red;
}
.bar {
color: blue;
}
@media (min-width: 1024px) {
.baz {
color: green;
}
@media (min-width: 1280px) {
.qux {
color: yellow;
}
}
}
}
}
```
Output:
```css
@utility foo {
@media (min-width: 640px) {
color: red;
}
}
@utility bar {
@media (min-width: 640px) {
color: blue;
}
}
@utility baz {
@media (min-width: 640px) {
@media (min-width: 1024px) {
color: green;
}
}
}
@utility qux {
@media (min-width: 640px) {
@media (min-width: 1024px) {
@media (min-width: 1280px) {
color: yellow;
}
}
}
}
```
---
When classes result in multiple `@utility` directives with the same
name, then the definitions will be merged together.
Input:
```css
@layer utilities {
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
}
```
Intermediate representation:
```css
@utility no-scrollbar {
&::-webkit-scrollbar {
display: none;
}
}
@utility no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
```
Output:
```css
@utility no-scrollbar {
&::-webkit-scrollbar {
display: none;
}
-ms-overflow-style: none;
scrollbar-width: none
}
```
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
This PR updates the `hover` variant to only apply when `@media (hover:
hover)` matches.
```diff
.hover\:bg-black {
&:hover {
+ @media (hover: hover) {
background: black;
+ }
}
}
```
This is technically a breaking change because you may have built your
site in a way where some interactions depend on hover (like opening a
dropdown menu), and were relying on the fact that tapping on mobile
triggers hover.
To bring back the old hover behavior, users can override the `hover`
variant in their CSS file back to the simpler implementation:
```css
@import "tailwindcss";
@variant hover (&:hover);
```
I've opted to go with just `@media (hover: hover)` for this because it
seems like the best trade-off between the available options. Using
`(any-hover: hover)` would mean users would get sticky hover states when
tapping on an iPad if they have a mouse or trackpad connected, which
feels wrong to me because in those cases touch is still likely the
primary method of interaction.
Sites built with this feature in mind will be treating hover styles as
progressive enhancement, so it seems better to me that using an iPad
with a mouse would not have hover styles, vs. having sticky hover styles
in the same situation.
Of course users can always override this with whatever they want, so
making this the default isn't locking anyone in to a particular choice.
---------
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This PR is a continuation of #13537.
Currently we reverted the merged changes so that we can get a new alpha
version out without this change.
---
This PR removes automatic `var(…)` injection for arbitrary properties,
values and modifiers.
There are a few properties that use "dashed-ident" values, this means
that you can use `--my-thing` as-is without the `var(…)` around it.
E.g.:
```css
.foo {
/* Notice that these don't have `var(…)` */
view-timeline-name: --timeline-name;
anchor-name: --sidebar;
}
```
This causes issues because we are now injecting a `var(…)` where it's
not needed.
One potential solution is to create a list of properties where dashed
idents can be used. However, they can _also_ use CSS custom properties
that point to another dashed ident.
E.g.:
```css
.foo {
--target: --sidebar;
anchor-name: var(--target);
}
```
A workaround that some people used is adding a `_` in front of the
variable: `mt-[_--my-thing]` this way we don't automatically inject the
`var(…)` around it. This is a workaround and gross.
While the idea of automatic var injection is neat, this causes more
trouble than it's worth. Adding `var(…)` explicitly is better.
A side effect of this is that we can simplify the internals for the
`candidate` data structure because we don't need to track `dashedIdent`
separately anymore.
<!--
👋 Hey, thanks for your interest in contributing to Tailwind!
**Please ask first before starting work on any significant new
features.**
It's never a fun experience to have your pull request declined after
investing a lot of time and effort into a new feature. To avoid this
from happening, we request that contributors create an issue to first
discuss any significant new features. This includes things like adding
new utilities, creating new at-rules, or adding new component examples
to the documentation.
https://github.com/tailwindcss/tailwindcss/blob/master/.github/CONTRIBUTING.md
-->
This fixes the following issue
https://github.com/tailwindlabs/tailwindcss/issues/14305 by using
`:dir()`
---------
Co-authored-by: Philipp Spiess <hello@philippspiess.com>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This PR brings `@import` resolution into Tailwind CSS core. This means
that our clients (PostCSS, Vite, and CLI) no longer need to depend on
`postcss` and `postcss-import` to resolve `@import`. Furthermore this
simplifies the handling of relative paths for `@source`, `@plugin`, or
`@config` in transitive CSS files (where the relative root should always
be relative to the CSS file that contains the directive). This PR also
fixes a plugin resolution bug where non-relative imports (e.g. directly
importing node modules like `@plugin '@tailwindcss/typography';`) would
not work in CSS files that are based in a different npm package.
### Resolving `@import`
The core of the `@import` resolution is inside
`packages/tailwindcss/src/at-import.ts`. There, to keep things
performant, we do a two-step process to resolve imports. Imagine the
following input CSS file:
```css
@import "tailwindcss/theme.css";
@import "tailwindcss/utilities.css";
```
Since our AST walks are synchronous, we will do a first traversal where
we start a loading request for each `@import` directive. Once all loads
are started, we will await the promise and do a second walk where we
actually replace the AST nodes with their resolved stylesheets. All of
this is recursive, so that `@import`-ed files can again `@import` other
files.
The core `@import` resolver also includes extensive test cases for
[various combinations of media query and supports conditionals as well
als layered
imports](https://developer.mozilla.org/en-US/docs/Web/CSS/@import).
When the same file is imported multiple times, the AST nodes are
duplicated but duplicate I/O is avoided on a per-file basis, so this
will only load one file, but include the `@theme` rules twice:
```css
@import "tailwindcss/theme.css";
@import "tailwindcss/theme.css";
```
### Adding a new `context` node to the AST
One limitation we had when working with the `postcss-import` plugin was
the need to do an additional traversal to rewrite relative `@source`,
`@plugin`, and `@config` directives. This was needed because we want
these paths to be relative to the CSS file that defines the directive
but when flattening a CSS file, this information is no longer part of
the stringifed CSS representation. We worked around this by rewriting
the content of these directives to be relative to the input CSS file,
which resulted in added complexity and caused a lot of issues with
Windows paths in the beginning.
Now that we are doing the `@import` resolution in core, we can use a
different data structure to persist this information. This PR adds a new
`context` node so that we can store arbitrary context like this inside
the Ast directly. This allows us to share information with the sub tree
_while doing the Ast walk_.
Here's an example of how the new `context` node can be used to share
information with subtrees:
```ts
const ast = [
rule('.foo', [decl('color', 'red')]),
context({ value: 'a' }, [
rule('.bar', [
decl('color', 'blue'),
context({ value: 'b' }, [
rule('.baz', [decl('color', 'green')]),
]),
]),
]),
]
walk(ast, (node, { context }) => {
if (node.kind !== 'declaration') return
switch (node.value) {
case 'red': assert(context.value === undefined)
case 'blue': assert(context.value === 'a')
case 'green': assert(context.value === 'b')
}
})
```
In core, we use this new Ast node specifically to persist the `base`
path of the current CSS file. We put the input CSS file `base` at the
root of the Ast and then overwrite the `base` on every `@import`
substitution.
### Removing the dependency on `postcss-import`
Now that we support `@import` resolution in core, our clients no longer
need a dependency on `postcss-import`. Furthermore, most dependencies
also don't need to know about `postcss` at all anymore (except the
PostCSS client, of course!).
This also means that our workaround for rewriting `@source`, the
`postcss-fix-relative-paths` plugin, can now go away as a shared
dependency between all of our clients. Note that we still have it for
the PostCSS plugin only, where it's possible that users already have
`postcss-import` running _before_ the `@tailwindcss/postcss` plugin.
Here's an example of the changes to the dependencies for our Vite client
✨ :
<img width="854" alt="Screenshot 2024-09-19 at 16 59 45"
src="https://github.com/user-attachments/assets/ae1f9d5f-d93a-4de9-9244-61af3aff1237">
### Performance
Since our Vite and CLI clients now no longer need to use `postcss` at
all, we have also measured a significant improvement to the initial
build times. For a small test setup that contains only a hand full of
files (nothing super-complex), we measured an improvement in the
**3.5x** range:
<img width="1334" alt="Screenshot 2024-09-19 at 14 52 49"
src="https://github.com/user-attachments/assets/06071fb0-7f2a-4de6-8ec8-f202d2cc78e5">
The code for this is in the commit history if you want to reproduce the
results. The test was based on the Vite client.
### Caveats
One thing to note is that we previously relied on finding specific
symbols in the input CSS to _bail out of Tailwind processing
completely_. E.g. if a file does not contain a `@tailwind` or `@apply`
directive, it can never be a Tailwind file.
Since we no longer have a string representation of the flattened CSS
file, we can no longer do this check. However, the current
implementation was already inconsistent with differences on the allowed
symbol list between our clients. Ideally, Tailwind CSS should figure out
wether a CSS file is a Tailwind CSS file. This, however, is left as an
improvement for a future API since it goes hand-in-hand with our planned
API changes for the core `tailwindcss` package.
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
This PR changes the behavior of the `transition-{property}` utilities to
respect any explicit timing function or duration set by the user using
the `ease-*` and `duration-*` utilities.
Say you have this HTML:
```html
<div class="transition-colors duration-500 ease-out lg:transition-all">
```
Currently, the `transition-duration` and `transition-timing-functions`
will be reset to their default values at the `lg` breakpoint even though
you've provided explicit values for them.
After this PR is merged, those values will be preserved at the `lg`
breakpoint.
This PR also adds `duration-initial` and `ease-initial` utilities to
"unset" explicit duration/timing-function values so that the defaults
from classes like `transition-all` will kick in, without having to
specify their explicit values.
---------
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
This PR adds our custom gradient color properties (`--tw-gradient-from`,
`--tw-gradient-via`, and `--tw-gradient-to`) to the list of color
properties we transition in the `transition` and `transition-colors`
utilities.
As part of this I noticed that we had duplicate `@property` declarations
for these custom properties, so I've removed the duplicates.
---------
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
- Avoid cross-device copying in Windows CI by setting the tests dir to
the same drive as the workspace.
- Disable LTO and use a faster linker for the Rust build
Buid: ~3min -> ~2min
Integration Tests: ~8min -> ~3min20s
Fixes#14479.
Back in March we made a change to the `transition-*` utilities that
inlined the values of the `--default-transition-*` variables to fix a
bug where things would break if those variables didn't exist in your
CSS. At the time though we weren't outputting CSS variables as part of
the values of any utilities, for example `bg-red-500` didn't actually
reference the `--color-red-500` variable.
We later changed that but missed this situation, so these variables were
still inlined even though nothing else was.
This PR fixes that and makes things more consistent, so these variables
will be used as expected unless using the `@theme inline` option, like
with everything else.
---------
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
This PR adds support for the
[`field-sizing`](https://developer.mozilla.org/en-US/docs/Web/CSS/field-sizing)
property which can be used to fit a text inputs, file inputs, textareas,
and selects to the size of the text rather than some implicit default
width.
---------
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
<!--
👋 Hey, thanks for your interest in contributing to Tailwind!
**Please ask first before starting work on any significant new
features.**
It's never a fun experience to have your pull request declined after
investing a lot of time and effort into a new feature. To avoid this
from happening, we request that contributors create an issue to first
discuss any significant new features. This includes things like adding
new utilities, creating new at-rules, or adding new component examples
to the documentation.
https://github.com/tailwindcss/tailwindcss/blob/master/.github/CONTRIBUTING.md
-->
```ts
// error: can't find `@tailwindcss/node` types file
import { compile } from '@tailwindcss/node';
```
This PR complements #14458 by adding new `shadow-initial` and
`inset-shadow-initial` utilities that make it possible to "undo" a
custom shadow color and revert to the default shadow color for the
current shadow size.
For example, in this example the shadow will be red on hover even though
the default color for the `shadow` utility is `rgb(0 0 0 / 5%)`:
```html
<div class="shadow-sm shadow-red-500 hover:shadow">
<!-- … -->
</div>
```
This is usually what you want, but if you actually want `hover:shadow`
to apply its default color, you need to know what the color is and add
it yourself:
```html
<div class="shadow-sm shadow-red-500 hover:shadow hover:shadow-black/5">
<!-- … -->
</div>
```
Using `shadow-initial`, you can achieve the same thing without having to
know what the original color was:
```html
<div class="shadow-sm shadow-red-500 hover:shadow hover:shadow-initial">
<!-- … -->
</div>
```
The `inset-shadow-initial` utility does the same thing for the
`inset-shadow-*` utilities.
---------
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
This PR adds new `bg-radial-*` and `bg-conic-*` utilities for radial and
conic gradients. It also adds support for "arbitrary gradients", where
gradient utilities like `bg-linear-*`, `bg-radial-*`, and `bg-conic-*`
can now accept a complete gradient definition as their arbitrary value.
## Radial gradients
Radial gradients are created with the `bg-radial` utility, or the
`bg-radial-[…]` utility, combined with the existing `from-*`, `via-*`,
and `to-*` utilities.
The simple `bg-radial` utility just creates a radial gradient with no
position information, which defaults to `center`:
```
radial-gradient({from}, {via}, {to});
```
If you use the arbitrary value format, whatever you provide as the
arbitrary value is inserted into the first position:
```
radial-gradient({arbitrary value}, {from}, {via}, {to});
```
So a utility like `bg-radial-[at_top_left]` would produce this:
```
radial-gradient(at top left, {from}, {via}, {to});
```
This makes it possible to use some of the `radial-gradient(…)` features
that this PR doesn't add first class support for, like using values like
`circle at center` or providing a specific interpolation color space
like `in hsl longer hue`. We may add explicit APIs for these in the
future, but I'm proposing this PR first since those changes would be
purely additive and none of the decisions here would create any conflict
with those APIs.
## Conic gradients
Conic gradients are created with the `bg-conic`,
`bg-conic-{bareNumber}`, and `bg-conic-[…]` utilities, combined with the
existing `from-*`, `via-*`, and `to-*` utilities.
The `bg-conic` utility creates a conic gradient with no angle, which
defaults to `0deg`:
```
conic-gradient({from}, {via}, {to});
```
The `bg-conic-{bareNumber}` utilities create conic gradients using the
bare number as the angle:
```
conic-gradient(from {bareNumber}deg, {from}, {via}, {to});
```
The `bg-conic-[…]` arbitrary value utilities insert whatever you provide
as the arbitrary value into the first position verbatim:
```
conic-gradient({arbitraryValue}, {from}, {via}, {to});
```
So a utility like `bg-conic-[from_45deg_in_hsl]` would produce this:
```
conic-gradient(from 45deg in hsl, {from}, {via}, {to});
```
Note that the `from` keyword needs to be provided by the user when using
arbitrary values, but not when using bare values.
This makes it possible to use some of the `conic-gradient(…)` features
that this PR doesn't add first class support for, like using values like
`at 0 0` or providing a specific interpolation color space like `in hsl
longer hue`. We may add explicit APIs for these in the future, but I'm
proposing this PR first since those changes would be purely additive and
none of the decisions here would create any conflict with those APIs.
## Arbitrary gradients
Prior to this PR, utilities like `bg-linear-[…]` could only accept
positional information as their arbitrary value, like
`bg-linear-[to_top_right]`. All of the color stops could only be
provided using the `from-*`, `via-*`, and `to-*` utilities.
If you wanted to provide the complete gradient in one class, you needed
to use `bg-[…]` and write out the gradient function yourself:
```html
<div class="bg-[linear-gradient(to_right,var(--color-red-500),var(--color-yellow-400))]">
```
This PR refactors some things internally to make it possible to provide
the entire gradient as the arbitrary value to each background gradient
utility, like this:
```html
<div class="bg-linear-[to_right,var(--color-red-500),var(--color-yellow-400)]">
```
This is nice if you're doing something very custom and you want to be
able to look at the whole value together, while still avoiding some of
the boilerplate you'd have if you had to write out the entire gradient
function yourself.
---------
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
These changes were extracted from our work on a `screen()` function for
CSS — we've decided to move those changes to a code mod instead of
implementing support for `screen()` in the compiler — but the
refactoring around the changes still makes sense to do so I'm landing
that here separately.
---------
Co-authored-by: Philipp Spiess <hello@philippspiess.com>
This PR changes how shadow color and shadow size utilities interact when
used with variants.
Take this HTML:
```html
<div class="shadow-lg shadow-red-500 hover:shadow-xl">
<!-- … -->
</div>
```
Currently this shadow would be red by default, but revert to the default
semi-transparent black color on hover.
This PR changes this behavior such that the shadow remains red on hover,
and only the shadow size changes.
We deliberately didn't do this originally because making things behave
this way makes it very difficult to get the default shadow color back
once you've changed it. The default color for `shadow-xl` for instance
is `rgb(0 0 0 / 0.1)`, and the only way to get that color back after
changing it is to know that value and explicitly bring it back:
```html
<div class="shadow-lg shadow-red-500 hover:shadow-xl hover:shadow-black/10">
<!-- … -->
</div>
```
To make things more difficult, the default shadow color is not the same
across shadow sizes. For `shadow-sm` it's `black/5`, and for
`shadow-2xl` it's `black/25`.
In practice though you basically never need to bring back the default
shadow color, so I'm reconsidering this trade-off in v4, and think I
prefer this new behavior where the color is preserved but you have to
bring back the default color if you actually need it.
A simple workaround if you don't know the color is to reset the
`--tw-shadow-color` variable like this:
```html
<div class="shadow-lg shadow-red-500 hover:shadow-xl hover:[--tw-shadow-color:initial]">
<!-- … -->
</div>
```
This relies on semi-private internals though, so perhaps we can
introduce a utility for this, like `shadow-default` or `shadow-initial`
that just unsets the shadow color.
---------
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
This PR adds CSS codemods for migrating existing `@tailwind` directives
to the new alternatives.
This PR has the ability to migrate the following cases:
---
Typical default usage of `@tailwind` directives in v3.
Input:
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```
Output:
```css
@import 'tailwindcss';
```
---
Similar as above, but always using `@import` instead of `@import`
directly.
Input:
```css
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
```
Output:
```css
@import 'tailwindcss';
```
---
When you are _only_ using `@tailwind base`:
Input:
```css
@tailwind base;
```
Output:
```css
@import 'tailwindcss/theme' layer(theme);
@import 'tailwindcss/preflight' layer(base);
```
---
When you are _only_ using `@tailwind utilities`:
Input:
```css
@tailwind utilities;
```
Output:
```css
@import 'tailwindcss/utilities' layer(utilities);
```
---
If the default order changes (aka, `@tailwind utilities` was defined
_before_ `@tailwind base`), then an additional `@layer` will be added to
the top to re-define the default order.
Input:
```css
@tailwind utilities;
@tailwind base;
```
Output:
```css
@layer theme, components, utilities, base;
@import 'tailwindcss';
```
---
When you are _only_ using `@tailwind base; @tailwind utilities;`:
Input:
```css
@tailwind base;
@tailwind utilities;
```
Output:
```css
@import 'tailwindcss';
```
We currently don't have a concept of `@tailwind components` in v4, so if
you are not using `@tailwind components`, we can expand to the default
`@import 'tailwindcss';` instead of the individual imports.
---
`@tailwind screens` and `@tailwind variants` are not supported/necessary
in v4, so we can safely remove them.
Input:
```css
@tailwind screens;
@tailwind variants;
```
Output:
```css
```
This PR adds support for the _simple_ case of the `screens` option
inside JS config paths. This allows JS configs to extend the responsive
theme by adding custom breakpoints. Here's an example from our v3 docs:
```js
{
theme: {
screens: {
'sm': '640px',
// => @media (min-width: 640px) { ... }
'md': '768px',
// => @media (min-width: 768px) { ... }
'lg': '1024px',
// => @media (min-width: 1024px) { ... }
'xl': '1280px',
// => @media (min-width: 1280px) { ... }
'2xl': '1536px',
// => @media (min-width: 1536px) { ... }
}
}
}
```
For simple breakpoints, this will extend the core breakpoints and will
work with the `min-*` and `max-*` utilities. However, we also support
complex ways of setting up custom screens like this:
```js
{
theme: {
extend: {
screens: {
sm: { max: '639px' },
md: [
{ min: '668px', max: '767px' },
{ min: '868px' },
],
lg: { min: '868px' },
xl: { min: '1024px', max: '1279px' },
tall: { raw: '(min-height: 800px)' },
},
},
},
}
```
For these complex setups, we _only_ generate the shorthand variant (e.g.
`tall`) but those won't integrate within `min-*` and `max-*`. In v3,
adding any of these complex configurations would omit any `min-*` and
`max-*` variants.
Right now, it is possible to type `grid-cols--8` which maps to:
```css
/* Specificity: (0, 1, 0) */
.grid-cols--8 {
grid-template-columns: repeat(-8, minmax(0, 1fr));
}
```
This doesn't make sense so we used this opportunity to audit all
variants and utilities and properly disallow negative bare values.
Utilities where negative values are supported still work by using the
negative utility syntax, e.g.: `-inset-4`.
This PR adds some initial tooling for codemods. We are currently only
interested in migrating CSS files, so we will be using PostCSS under the
hood to do this. This PR also implements the "migrate `@apply`" codemod
from #14412.
The usage will look like this:
```sh
npx @tailwindcss/upgrade
```
You can pass in CSS files to transform as arguments:
```sh
npx @tailwindcss/upgrade src/**/*.css
```
But, if none are provided, it will search for CSS files in the current
directory and its subdirectories.
```
≈ tailwindcss v4.0.0-alpha.24
│ No files provided. Searching for CSS files in the current
│ directory and its subdirectories…
│ Migration complete. Verify the changes and commit them to
│ your repository.
```
The tooling also requires the Git repository to be in a clean state.
This is a common convention to ensure that everything is undo-able. If
we detect that the git repository is dirty, we will abort the migration.
```
≈ tailwindcss v4.0.0-alpha.24
│ Git directory is not clean. Please stash or commit your
│ changes before migrating.
│ You may use the `--force` flag to override this safety
│ check.
```
---
This PR alsoo adds CSS codemods for migrating existing `@apply`
directives to the new version.
This PR has the ability to migrate the following cases:
---
In v4, the convention is to put the important modifier `!` at the end of
the utility class instead of right before it. This makes it easier to
reason about, especially when you are variants.
Input:
```css
.foo {
@apply !flex flex-col! hover:!items-start items-center;
}
```
Output:
```css
.foo {
@apply flex! flex-col! hover:items-start! items-center;
}
```
---
In v4 we don't support `!important` as a marker at the end of `@apply`
directives. Instead, you can append the `!` to each utility class to
make it `!important`.
Input:
```css
.foo {
@apply flex flex-col !important;
}
```
Output:
```css
.foo {
@apply flex! flex-col!;
}
```
This PR improves how the `text-{size}` utilities interact with the
`leading-*`, `tracking-*`, and `font-{weight}` utilities, ensuring that
if the user explicitly uses any of those utilities that those values are
not squashed by any defaults baked into the `text-{size}` utilities.
Prior to this PR, if you wrote something like this:
```html
<div class="text-lg leading-none md:text-2xl">
```
…the `leading-none` class would be overridden by the default line-height
value baked into the `text-2xl` utility at the `md` breakpoint. This has
been a point of confusion and frustration for people [in the
past](https://github.com/tailwindlabs/tailwindcss/issues/6504) who are
annoyed they have to keep repeating their custom `leading-*` value like
this:
```html
<div class="text-lg leading-none md:text-2xl md:leading-none lg:text-4xl lg:leading-none">
```
This PR lets you write this HTML instead but get the same behavior as
above:
```html
<div class="text-lg leading-none md:text-2xl lg:text-4xl">
```
It's important to note that this change _only_ applies to line-height
values set explicitly with a `leading-*` utility, and does not apply to
the line-height modifier.
In this example, the line-height set by `text-sm/6` does _not_ override
the default line-height included in the `md:text-lg` utility:
```html
<div class="text-sm/6 md:text-lg">
```
That means these two code snippets behave differently:
```html
<div class="text-sm/6 md:text-lg">…</div>
<div class="text-sm leading-6 md:text-lg">…</div>
```
In the top one, the line-height `md:text-lg` overrides the line-height
set by `text-sm/6`, but in the bottom one, the explicit `leading-6`
utility takes precedence.
This PR applies the same improvements to `tracking-*` and
`font-{weight}` as well, since all font size utilities can also
optionally specify default `letter-spacing` and `font-weight` values.
We achieve this using new semi-private CSS variables like we do for
things like transforms, shadows, etc., which are set by the `leading-*`,
`tracking-*`, and `font-{weight}` utilities respectively. The
`text-{size}` utilities always use these values first if they are
defined, and the default values become fallbacks for those variables if
they aren't present.
We use `@property` to make sure these variables are reset to `initial`
on a per element basis so that they are never inherited, like with every
other variable we define.
This PR does slightly increase the amount of CSS generated, because now
utilities like `leading-5` look like this:
```diff
.leading-5 {
+ --tw-leading: 1.25rem;
line-height: 1.25rem;
}
```
…and utilites like `text-sm` include a `var(…)` lookup that they didn't
include before:
```diff
.text-sm {
font-size: 0.875rem;
- line-height: var(--font-size-sm--line-height, 1.25rem);
+ line-height: var(--tw-leading, var(--font-size-sm--line-height, 1.25rem));
}
```
If this extra CSS doesn't feel worth it for the small improvement in
behavior, we may consider just closing this PR and keeping things as
they are.
This PR is also a breaking change for anyone who was depending on the
old behavior, and expected the line-height baked into the `md:text-lg`
class to take precedence over the explicit `leading-6` class:
```html
<div class="text-sm leading-6 md:text-lg">…</div>
```
Personally I am comfortable with this because of the fact that you can
still get the old behavior by preferring a line-height modifier:
```html
<div class="text-sm/6 md:text-lg">…</div>
```
---------
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
Resolves#14440.
This PR fixes an issue where registering a custom `inset-shadow-*`
utility value in your theme like this:
```css
@theme {
--inset-shadow-potato: inset 0px 2px 6px #7a4724;
}
```
…mistakenly generates both an `inset-shadow-*` and `inset-*` utility
with that value:
```css
.inset-shadow-potato {
inset: inset 0px 2px 6px #7a4724;
}
.inset-shadow-potato {
box-shadow: inset 0px 2px 6px #7a4724;
}
```
This replaces #14445 which turns out to not be the ideal solution.
Now we just explicitly ignore variables like `--inset-shadow-*` and
`--inset-ring-*` in the `inset` handler. Kind of a gross patch but I can
live with it because the whole existence of the `--inset-*` key is kind
of a backwards compatibility thing anyways.
---------
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
Co-authored-by: Philipp Spiess <hello@philippspiess.com>
Right now when we encounter a candidates with invalid `theme()` calls we
throw an error which stops the build entirely. This is not ideal
because, especially in the case of `node_modules`, if one file in one
package has an error it will stop the build for an entire project and
tracking this down can be quite difficult.
Now, after this PR, any candidates that use `theme(…)` with non-existent
theme keys (e.g. `rounded-[theme(--radius-does-not-exist)]`) will be
skipped instead of breaking the build.
Before:
```html
<div class="underline rounded-[theme(--radius-does-not-exist)]"></div>
```
```css
/* No CSS was generated because an error was thrown */
/* Error: Invalid theme key: --radius-does-not-exist */
```
After:
```html
<div class="underline rounded-[theme(--radius-does-not-exist)]"></div>
```
```css
.underline {
text-decoration-line: underline;
}
```
---------
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
This PR fixes an issue where using `anchor-size` in arbitrary values
resulted in the incorrect css.
Input: `w-[calc(anchor-size(width)+8px)]`
Output:
```css
.w-\[calc\(anchor-size\(width\)\+8px\)\] {
width: calc(anchor - size(width) + 8px);
}
```
This PR fixes that, by generating the correct CSS:
```css
.w-\[calc\(anchor-size\(width\)\+8px\)\] {
width: calc(anchor-size(width) + 8px);
}
```
This PR adds support for the `aria`, `supports`, and `data` properties
found in JS config options. In v3, you could extend the theme to add
more variants by using an object syntax like this:
```ts
{
theme: {
extend: {
aria: {
polite: 'live="polite"',
},
supports: {
'child-combinator': 'h2 > p',
},
data: {
checked: 'ui~="checked"',
},
},
}
}
```
Since we no longer rely on theme variables for these variants, the way
to make this work is by adding custom variants for each of these
manually added variants.
---------
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
This PR fixes an issue with the order of CSS when using stacked variants
when two variants have the same order (as defined by the custom
comperator function).
## The problem
Take, for example, our breakpoint variants. Those are split into `max-*`
variants and a group containing all `min-*` variants as well as the
unprefixed static ones (e.g. `lg`, `sm`).
We currently define a custom sort order for all breakpoints variants
that will compare their order based on the resolved value provided. So
if you define `--breakpoint-sm: 100px` and `--breakpoint-lg: 200px`, we
first check if both breakpoints have the same unit and then we rank
based on the numerical value, making `sm` appear before `lg`.
But since the `min-*` variant and the `sm` variant share the same group,
this also means that `min-sm` and `sm` as well as `min-lg` and `lg` will
always have the same order (which makes sense—they also have the exact
same CSS they generate!)
The issue now arises when you use these together with variant stacking.
So, say you want to stack the two variants `max-lg:min-sm`. We always
want stacked variants to appear _after_ their non-stacked individual
parts (since they are more specific). To do this right now, we generate
a bitfield based on the variant order. If you have four variants like
this:
| Order | Variant |
| ------------- | ------------- |
| 0 | `max-lg` |
| 1 | `max-sm` |
| 2 | `min-sm` |
| 3 | `min-lg` |
We will assign one bit for each used variant starting from the lowest
bit, so for the stack `max-lg:min-sm` we will set the bitfield to `0101`
and those for the individual variants would result in `0100` (for
`min-sm`) and `0001` (for `max-lg`). We then convert this bitfield to a
number and order based on that number. This ensures that the stack
always sorts higher.
The issue now arises from the fact that the variant order also include
the unprefixed variants for a breakpoint. So in our case of `lg` and
`sm`, the full list would look like this:
| Order | Variant |
| ------------- | ------------- |
| 0 | `max-lg` |
| 1 | `max-sm` |
| 2 | `min-sm` |
| 3 | `sm` |
| 4 | `min-lg` |
| 5 | `lg` |
This logic now breaks when you start to compute a stack for something
like `max-lg:min-lg` _while also using the `lg` utility:
| Stack | Bitmap | Integer Value |
| ------------- | ------------- | ------------- |
| `max-lg:min-lg` | `010001` | 17 |
| `lg` | `100000` | 18 |
As you can see here, the sole `lg` variant will now sort higher than the
compound of `max-lg:min-lg`. That's not something we want!
## Proposed solution
To fix this, we need to encode the information of _same_ variant order
somehow. A single array like the example above is not sufficient for
this, since it will remove the information of the similar sort order.
Instead, we now computed a list of nested arrays for the order lookup
that will combine variants of similar values (while keeping the order
the same). So from the 6 item array above, we now have the following
nested array:
| Order | Variant |
| ------------- | ------------- |
| 0 | [`max-lg`] |
| 1 | [`max-sm`] |
| 2 | [`min-sm`, `sm`] |
| 3 | [`min-lg`, `lg`] |
When we use the first layer index for the bitfield, we can now see how
this solves the issue:
| Stack | Bitmap | Integer Value |
| ------------- | ------------- | ------------- |
| `max-lg:min-lg` | `1001` | 9 |
| `lg` | `1000` | 8 |
That's pretty-much it! There are a few other changes in this PR that
mostly handles with a small regression by this change where now, named
`group` variants and unnamed `group` variants would now have the same
order (something that was undefined behavior before).
---------
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
Fixes an issue where `borderRadius` was not properly upgraded when using
it in the `theme()` function like this:
```
rounded-[theme(borderRadius.lg)]
```
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
This PR builds on top of #14365 and adds a few more changes we discussed
during a sync on the latter PR:
- We now split `plugin-api.ts` into two files and moved it into
`compat/`. One file is now defining the comat plugin API only where as
another file deals with the compat hook.
- The walk for `@plugin` and `@config` is now happening inside the
compat hook.
The one remaining work item is to change the `loadPlugin` and
`loadConfig` APIs to a more unified `resolveModule` one that does not
care on what we try to load it for. I suggest we should make this change
at the same time we start working on finalizing the `tailwindcss` APIs,
since a lot of things will have to be rethought then anyways.
This PR works around a current regression in the Rust toolchain that
caused our Windows workers to start failing with:
```
Finished `test` profile [unoptimized + debuginfo] target(s) in 32.63s
Running unittests src\lib.rs (target\debug\deps\tailwind_oxide-ce6a5d43a3798437.exe)
Load Node-API [napi_get_last_error_info] from host runtime failed: GetProcAddress failed
fatal runtime error: thread::set_current should only be called once per thread
Load Node-API [napi_get_uv_event_loop] from host runtime failed: GetProcAddress failed
Load Node-API [napi_fatal_exception] from host runtime failed: GetProcAddress failed
Load Node-API [napi_create_threadsafe_function] from host runtime failed: GetProcAddress failed
error: test failed, to rerun pass `-p tailwind-oxide --lib`
```
The workaround is to pin the rust toolchain version so that the
regression isn't applied when we build on Windows in test mode.
While upgrading a project to Tailwind CSS v4, I forgot to remove the
`tailwindcss` import from the PostCSS config. As a result of this, I was
greeted with the following message:
```
node:internal/process/promises:289
triggerUncaughtException(err, true /* fromPromise */);
^
[Failed to load PostCSS config: Failed to load PostCSS config (searchPath: /Users/philipp/dev/project): [TypeError] Invalid PostCSS Plugin found at: plugins[0]
(@/Users/philipp/dev/project/postcss.config.js)
TypeError: Invalid PostCSS Plugin found at: plugins[0]
```
I don't think this was particularly helpful, so I’m proposing we add a
default function export to the `tailwindcss` package so when it's used
inside PostCSS, we can control the error message. So I changed it to
something along these lines:
```
It looks like you're trying to use the \`tailwindcss\` package as a PostCSS plugin. This is no longer possible since Tailwind CSS v4.
If you want to continue to use Tailwind CSS with PostCSS, please install \`@tailwindcss/postcss\` and change your PostCSS config file.
at w (/Users/philipp/dev/project/node_modules/tailwindcss/node_modules/tailwindcss/dist/lib.js:1:21233)
at Object.<anonymous> (/Users/philipp/dev/project/node_modules/tailwindcss/postcss.config.cjs:3:13)
at Module._compile (node:internal/modules/cjs/loader:1358:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1416:10)
at Module.load (node:internal/modules/cjs/loader:1208:32)
at Module._load (node:internal/modules/cjs/loader:1024:12)
at cjsLoader (node:internal/modules/esm/translators:348:17)
at ModuleWrap.<anonymous> (node:internal/modules/esm/translators:297:7)
at ModuleJob.run (node:internal/modules/esm/module_job:222:25)
at async ModuleLoader.import (node:internal/modules/esm/loader:316:24)
```
This is also a good place to link to the migration guides once we have
them 🙂
---------
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
This PR adds support for the `matchVariant` plugin API. I've copied over
all [V3
tests](f07dbff2a7/tests/match-variants.test.js)
and made sure they still pass.
## Sorted order of stacked arbitrary variants
The only difference in behavior is regarding the sort order of stacked
arbitrary variants: Sorting in this case now works by the latest defined
`matchVariant` taking precedence.
So, if you define a plugin like this:
```ts
matchVariant('testmin', (value) => `@media (min-width: ${value})`, {
sort(a, z) {
return parseInt(a.value) - parseInt(z.value)
},
})
matchVariant('testmax', (value) => `@media (max-width: ${value})`, {
sort(a, z) {
return parseInt(z.value) - parseInt(a.value)
},
})
```
The resulting CSS is first sorted by the `testmax` values descending and
then the `testmin` values ascending, so these candidates:
```txt
testmin-[150px]:testmax-[400px]:order-2
testmin-[100px]:testmax-[350px]:order-3
testmin-[100px]:testmax-[300px]:order-4
testmin-[100px]:testmax-[400px]:order-1
```
Will resolve to the order outlined by the `order-` utility.
## At-rules and placeholders support
Since we added support for at-rules and placeholders in the
`matchVariant` syntax like this:
```ts
matchVariant(
'potato',
(flavor) => `@media (potato: ${flavor}) { @supports (font:bold) { &:large-potato } }`,
)
```
We also added support for the same syntax to the `addVariant` API:
```ts
addVariant(
'potato',
'@media (max-width: 400px) { @supports (font:bold) { &:large-potato } }',
)
```
The only change necessary in core was to call functional variants for
when the variant value is set to `null`. This allows functional variants
to define the un-parameterized implementation like `potato:underline` as
opposed to `potato[big]:underline`.
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
I noticed a lot more backwards compatibility concerns had started
leaking into core, especially around the `theme` function, so did a bit
of work to try and pull that stuff out and into the compatibility layer.
Now the core version of `theme` only handles CSS variables (like
`--color-red-500`) and has no knowledge of the dot notation or how to
upgrade it. Instead, we unconditionally override that function in the
compatibility layer with a light version that _does_ know how to do the
dot notation upgrade, and override that again with the very heavy/slow
version that handles JS config objects only if plugins/JS configs are
actually used.
I've also renamed `registerPlugins` to `applyCompatibilityHooks` because
the name was definitely a bit out of date given how much work it's doing
now, and now call it unconditionally from core, leaving that function to
do any conditional optimizations itself internally.
Next steps I think would be to split up `plugin-api.ts` a bit and maybe
make `applyCompatibilityHooks` its own file, and move both of those
files into the `compat` folder so everything is truly isolated there.
My goal with this stuff is that if/when we ever decide to drop backwards
compatibility with these features in the future (maybe v5), that all we
have to do is delete the one line of code that calls
`applyCompatibilityHooks` in `index.ts`, and delete the `compat` folder
and we're done. I could be convinced that this isn't a worthwhile goal
if we feel it's making the codebase needlessly complex, so open to that
discussion as well.
---------
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
The internal `registerPlugins()` API is used to enable backwards
compatibility with v3 plugins and configs and it is called on every
build even when no v3 plugins or configs are used. This function has a
non-trivial cost in that case — around 5ms.
So this PR does a few things:
## Implements a simpler, faster `theme(…)` function
We now have a much simpler `theme(…)` function that can be used when
backwards compatibility is not necessary. It still supports many of the
same features:
- The modern, v4 style CSS variable syntax `theme(--color-red-500)`
- The legacy, v3 path style `theme(colors.red.500)`
- And the v3-style alpha modifier `theme(colors.red.500 / 50%)`
- Path upgrades so things like `theme(accentColor.red.500)` pulls from
`--color-red-500` when no `--accent-color-red-500` theme key exists
When you do have plugins or configs the more advanced `theme(…)`
function is swapped in for more complete backwards compatibility.
## `registerPlugins` registers globs
Before `registerPlugins` passed the `ResolvedConfig` out so we could
register globs in `compile()`. Since that one function is really the
main driver for backwards compat we decided to move the content path
registration into `registerPlugins` itself when it comes to paths
provided by plugins and configs.
This is an internal implementation detail (well this entire PR is) but
it's worth mentioning. This method is used to resolve a theme value from
a theme key.
## `registerPlugins` is now only called when necessary
All of the above work made it so that `registerPlugins` can be called
only as needed. This means that when no v3 plugins or configs are used,
`registerPlugins` is never called thus elminating the performance impact
of config resolution.
---------
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
Right now the following does not work and instead produces a type error:
```
import { type Config } from 'tailwindcss'
export default {
// … config here
} satisfies Config
```
We were not exporting a `Config` type but thankfully this already exists
in the codebase so we just need to export it.
It does _not_ have all properties of an existing config as not all
features have been implemented (or in some cases necessary / relevant
for v4).
Notably missing are:
- `important`
- `prefix`
- `separator`
- `safelist`
- `blocklist`
- `future`
- `experimental`
- `corePlugins`
Also, explicit keys for theme are not currently specified but we should
probably bring this back even if just as an auto-complete aid.
Previously, given the following CSS and configuration:
```css
/* app.css */
@theme default {
--font-size-base: 1.25rem;
--font-size-base--line-height: 1.5rem;
}
@tailwind utilities;
@config "./config.js";
```
```js
// config.js
export default {
theme: {
fontSize: {
// …
base: ['1rem', { lineHeight: '1.75rem' }],
},
// …
},
};
```
When a config or a plugin asked for the value of `theme(fontSize.base)`
like so:
```js
// config.js
export default {
theme: {
// …
typography: ({ theme }) => ({
css: {
'[class~="lead"]': {
fontSize: theme('fontSize.base')[0],
...theme('fontSize.base')[1],
},
}
}),
},
};
```
We would instead pull the values from the CSS theme even through they're
marked with `@theme default`. This would cause the incorrect font size
and line height to be used resulting in something like this (in the case
of the typography plugin with custom styles):
```css
.prose [class~="lead"] {
font-size: 1.25rem;
line-height: 1.5rem;
}
```
After this change we'll now pull the values from the appropriate place
(the config in this case) and the correct font size and line height will
be used:
```css
.prose [class~="lead"] {
font-size: 1rem;
line-height: 1.75rem;
}
```
This will work even when some values are overridden in the CSS theme:
```css
/* app.css */
@theme default {
--font-size-base: 1.25rem;
--font-size-base--line-height: 1.5rem;
}
@theme {
--font-size-base: 2rem;
}
@tailwind utilities;
@config "./config.js";
```
which would result in the following CSS:
```css
.prose [class~="lead"] {
font-size: 2rem;
line-height: 1.75rem;
}
```
---------
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
This PR fixes an issue where theme function calls like
`theme('transitionTimingFunction.DEFAULT')` would incorrectly resolve to
an object when the set of defined CSS theme values looked like this:
```css
@theme {
--transition-timing-function-in: ease-in;
--transition-timing-function-out: ease-out;
--transition-timing-function-in-out: ease-out;
}
```
We were mistakenly retrieving the entire
`--transition-timing-function-*` namespace in this case and returning an
object, even though the user is explicitly asking for a single value by
including `.DEFAULT` in their call.
This ensures it resolves to null instead. Fixes an issue I ran into on
this live stream earlier today:
https://x.com/adamwathan/status/1831740214051799281
---------
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>