-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for @apply
with complex classes, including responsive and pseudo-class variants
#2159
Conversation
@apply
with complex classes, including responsive and pseudo-class variants
This looks great 🔥🚀 |
Just today thought it'll be nice if hover resides inside the @apply |
This is the new era of Tailwind 🍃 |
This PR is a work of art. 👏
/* Input */
.foo {
@apply bg-white bg-black;
}
.bar {
@apply bg-black bg-white;
}
/* Output */
.foo {
background-color: black;
background-color: white;
}
.bar {
background-color: black;
background-color: white;
} I’m curious about this @adamwathan (and not currently on my computer to test it), but could you get the previous behavior by splitting the .foo {
@apply bg-white;
@apply bg-black;
} I’m assuming this has the following output because of the “Declarations are always inserted relative to the position of @apply” behavior, but I could be wrong: .foo {
background-color: white;
background-color: black;
} |
@benface Yep you are correct! You can use multiple apply rules as a sort of escape hatch 👍🏻 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Found the PR interesting and was curious, had some questions
` | ||
@tailwind base; | ||
@tailwind components; | ||
@tailwind utilities; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this means custom tailwind declarations won’t be available in VUE components?
Also does this need to be done again and again for each component?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean by "custom tailwind declarations"? If you mean custom CSS classes created in like a main.css
somewhere then yes, and this has been the case for years and is the fault of the build tooling on the webpack/whatever side.
Tools like Vue run PostCSS in isolation for every single <style>
block in a Vue component. So Tailwind is "firewalled" inside of that process and has no idea you even have a main.css
file annoyingly. In a perfect world, the build tooling on the Vue side would concatenate all of the CSS and then run PostCSS once for all of the CSS together. Unfortunately it doesn't so we have to do performance-anti-pattern work-arounds like this to meet users' expectations of things like @apply
still working.
And unfortunately
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean if you have more @tailwind foo
declarations than the default 3 (or even less if you Eg don’t want the base styles) [edge cases I know].
I mean in theory you could cheat using the file system or changing the post css configuration on the fly to point to a cache file of the rules.
When will this be released? Setting
with tailwind 1.6.2 doesn't do anything atm |
This is one of my favourite new features - Thanks! Just tried it and all works as expected except for use with the UPDATE: e.g. The error I receive on SOLVED: when I explicitly added |
Not sure where else to ask this, so I'll do it here. With this enabled, buttons that I have extracted like so: .button {
@apply text-center inline-block p-3 text-base rounded;
&:hover {
@apply underline;
}
&.disabled,
&:disabled,
&[disabled] {
@apply cursor-not-allowed no-underline;
&:hover {
@apply no-underline;
}
}
}
@responsive {
.button {
@apply bg-blue-500 text-white;
&:hover:not(.disabled):not(:disabled):not([disabled]) {
@apply bg-blue-600 text-white;
}
&.disabled,
&:disabled,
&[disabled] {
@apply bg-blue-100 text-blue-500;
}
&.button-inverse {
@apply bg-white text-blue-500 border border-blue-600;
&.disabled,
&:disabled,
&[disabled] {
@apply bg-white text-blue-300 border border-blue-400;
}
}
}
.button-red {
@apply bg-red-500 text-white;
&:hover:not(.disabled):not(:disabled):not([disabled]) {
@apply bg-red-600 text-white;
}
&.disabled,
&:disabled,
&[disabled] {
@apply bg-red-100 text-red-500;
}
&.button-inverse {
@apply bg-white text-red-500 border border-red-600;
&.disabled,
&:disabled,
&[disabled] {
@apply bg-white text-red-300 border border-red-400;
}
}
}
} My <button type="button" class="button button-red">Danger, Will Robinson! Danger!</button> Is this intended? Do I just need to make my new colors more specific to override the default button's colors? |
Do you mind opening a new issue? Easier to make sure we actually give you a response and don't forget. Can't test right this second unfortunately. |
Sure, no problem. |
Nevermind, I figured it out. I have another class for dashboard buttons that is applying my |
This PR introduces support for using
@apply
with any class, and finally (3 years later!) resolves #313, which is probably the most requested feature in the history of the framework.TL;DR, this works now:
Pending merge, this will be available under a feature flag in Tailwind 1.x until it becomes the default in Tailwind 2.0 in the Fall:
Motivation
Previously, this sort of code would throw an error:
You would have to write this instead:
This totally kills the "copy the classes from your HTML class attribute and paste it after
@apply
" workflow that the framework promises.There were also a handful of other classes you couldn't apply that have always surprised people, like
clearfix
, or more recently the space between and divide utilities likespace-x-4
anddivide-y-2
.This is because of a fundamental limitation with the original implementation that prevented
@apply
from working with any selector that contained anything more than a single class name and was at the root of the CSS tree (so not nested within an at-rule like a media query).This is a major source of confusion for new users, and we get new GitHub issues and Discord questions all the time that can be traced back to this fundamental problem
New functionality
This PR makes it possible to use
@apply
with any class:Responsive variants
The
@apply
directive can now be used with all of Tailwind's responsive utilities, likemd:text-center
, and even things likemd:hover:opacity-50
:Pseudo-class variants
The
@apply
directive can now be used with pseudo-class variants, likehover:opacity-75
:Complex selectors
The
@apply
directive can now be used to apply classes that appear in complex selectors:Classes used in multiple rules
The
@apply
directive can now be used to apply classes that appear in multiple rules:This makes it possible to
@apply
thecontainer
class for example:Recursive
@apply
The
@apply
directive can now be used recursivelyDetailed explanation of behavior
This new implementation is designed around a single guiding principle:
Sounds simple in theory but following it does lead to behavior you might initially find unintuitive or think is undesirable, so let's discuss the particularly controversial implications...
All declarations relating to a class are included
Following the guiding principle, we now apply the declarations from all rules where the class being applied is included.
To understand why this is the correct behavior, consider this example:
If we wanted to extract a new class here, we would write this CSS:
Now we can replace the class list like so:
Now consider what happens if in the original example, we add the
is-active
class:This gives the link a yellow background color. If we are following the guiding principle outlined above, then this should also have a yellow background color:
If it doesn't, then the extraction we performed wasn't safe — it changed the behavior of the design.
For that reason, it is important that given this input:
...we generate this output:
Applied classes follow CSS source order, not apply order
Consider this example:
Both of these
div
elements will have a white background color, because the order of classes in the HTML does not matter. What matters is the order of the rules in the stylesheet (and specificity, but that's not relevant to this example).So following that, this input CSS:
...needs to generate this output:
Otherwise the behavior of
@apply
will differ from the behavior of using the utilities in your HTML, which breaks our guiding principle and means extracting with@apply
is not a pure refactoring.The shadow lookup table is merged with the user's CSS, not treated as a fallback
Prior to this PR, Tailwind would use a "shadow lookup table" to find a utility that was used in
@apply
if it couldn't find it in the user's CSS tree.In practice this was useful for Vue components, where you might try to use
@apply
in the<style>
block, because Vue runs PostCSS for each style block in isolation, which means Tailwind can't "see" anything outside of that block, so@apply flex
would fail because there was noflex
class in the style block.In this PR, the shadow lookup table still exists to support this use case, but because you can apply classes that appear in multiple rules, we prepend the lookup table to your custom CSS. This means it is not treated as a fallback, and will always be considered, even if Tailwind finds the class you are trying to apply in your custom CSS.
Consider this weird situation that you better not be doing you animal:
<!-- Some Vue component --> <template> <!-- ... --> </template> <style> .pt-4 { background-color: lol; } .foo { @apply pt-4; } </style>
In Tailwind currently, this would compile to this CSS output:
This is because when Tailwind tried to find
pt-4
, it found your custom version of it and used that. It doesn't consider the defaultpt-4
unless it can't findpt-4
in your own CSS.In this PR, you'd get this output:
Odds of this affecting you are basically zero, but it's a different mental model so worth explaining.
Related, Tailwind only prepends the shadow lookup if it cannot find evidence of Tailwind's styles existing in your CSS tree. The way it checks this is if your CSS contains any
@tailwind
rules.If Tailwind finds a single
@tailwind
rule in the tree, it will not prepend the lookup table, and will only search in your CSS.Declarations are always inserted relative to the position of
@apply
Consider this input:
This PR generates this output:
Seem a little confusing? Think about it in expanded form:
All of the classes added with
@apply
are inserted at the position of@apply
itself, even if that means pushing existing declarations out of the parent rule and into a clone of that parent rule that is added after the last@apply
-related rule.This is a weird one because it sort of breaks the guiding principle, and you could argue that the applied declarations should be added relative to the source order of the class you are applying into, but after a bunch of trial and error, this felt the most intuitive to us.
Breaking changes
This PR necessitates a few small breaking changes to how
@apply
currently works. They will affect very few people.Applied classes now follow source order
Like discussed above, the order of utilities after
@apply
no longer maps to the order that the declarations are actually inserted.In Tailwind currently, things work like this:
With this PR, things will work like this:
This new behavior is without a doubt correct if we agree that
@apply
is a tool for extracting a list of classes, and I would argue the current behavior is almost a bug.When migrating to this new implementation, you will need to be careful that you were not relying on the old behavior. In practice it is very unlikely you actually depended on this behavior anyways, you would almost never apply two classes that targeted the same CSS property, and if you did it would be situations like this:
...which you would have had to explicitly put in that order to get it to work, and Tailwind uses that order out of the box.
Can't apply utilities without your configured prefix
If you have a prefix configured in your
tailwind.config.js
file, you need to include that prefix when using@apply
:Previously this was optional, but now because Tailwind supports applying classes that appear in multiple rules, it's impossible for this behavior to not be ambiguous so we are removing it as a result. It breaks the guiding principle anyways, as using unprefixed classes in your HTML does not work if you have the prefix configured.
Using a leading dot in front of utilities is no longer supported
This no longer works with the new implementation:
We can make this work if there's some really good reason, but it hasn't been documented for like two years. No good reason to support both syntaxes.
Interoperability with the old
@apply
that used to be included in cssnext has been removedThere used to be a draft for an
@apply
rule that worked with these custom property bag things like this:Prior to this PR, Tailwind supported mixing that functionality with Tailwind's version of
@apply
, so you could do this:That proposal is deprecated and that feature will never exist now, so we have removed interop support.
FAQ
The most common question I've seen so far is "why aren't declarations with the same selector being grouped together?" in situations like this:
The simple answer is that this is the only way to make the applied class order match the CSS source order, because that is the order those rules appear in the CSS.
It's true that it is safe to optimize this in this case and collapse them, because there are no conflicting properties, but I consider this to be outside the scope of Tailwind itself. Use cssnano or CleanCSS for this, you should be using one of them for your production builds anyways and they are very smart dedicated tools that handle this beautifully.
Release strategy
Because this includes breaking changes it is slated for Tailwind 2.0, but will be available to try as an experimental feature in the next release under the
applyComplexClasses
key:We will release it as experimental for now so we can make changes if necessary once it's out in the wild and people can provide feedback, then we will promote it to
future
when it is stable, and finally turn it on by default in Tailwind 2.0.