Skip to content
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

Merged
merged 20 commits into from Aug 15, 2020

Conversation

adamwathan
Copy link
Member

@adamwathan adamwathan commented Aug 12, 2020

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:

.btn {
  @apply bg-indigo hover:bg-indigo-700 sm:text-lg;
}

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:

// tailwind.config.js
module.exports = {
  experimental: {
    applyComplexClasses: true,
  },
}

Motivation

Previously, this sort of code would throw an error:

.btn {
  @apply bg-indigo-600 hover:bg-indigo-700;
}

You would have to write this instead:

.btn {
  @apply bg-indigo-600;
}
.btn:hover {
  @apply hover:bg-indigo-700;
}

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 like space-x-4 and divide-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:

/* Input */
.btn {
  @apply bg-indigo-600 hover:bg-indigo-700 group-hover:opacity-50 sm:text-lg;
}

/* Output */
.btn {
  background-color: #5a67d8;
}
.btn:hover {
  background-color: #4c51bf;
}
.group:hover .btn {
  opacity: 0.5;
}
@media (min-width: 640px) {
  .btn {
    font-size: 1.125rem;
  }
}

Responsive variants

The @apply directive can now be used with all of Tailwind's responsive utilities, like md:text-center, and even things like md:hover:opacity-50:

/* Input */
.btn {
  @apply md:text-center md:hover:opacity-50;
}

/* Output */
@media (min-width: 768px) {
  .btn {
    text-align: center;
  }
  .btn:hover {
    opacity: 0.5;
  }
}

Pseudo-class variants

The @apply directive can now be used with pseudo-class variants, like hover:opacity-75:

/* Input */
.btn {
  @apply hover:opacity-50;
}

/* Output */
.btn:hover {
  opacity: 0.5;
}

Complex selectors

The @apply directive can now be used to apply classes that appear in complex selectors:

/* Input */
.btn {
  @apply complex-class;
}

.foo .complex-class:hover * {
  color: red;
}

/* Output */
.foo .btn:hover * {
  color: red;
}

.foo .complex-class:hover * {
  color: red;
}

Classes used in multiple rules

The @apply directive can now be used to apply classes that appear in multiple rules:

/* Input */
.btn {
  @apply example-class;
}

.example-class {
  background: red;
  font-weight: bold;
}

.example-class.is-active {
  opacity: 1;
}

@media (min-width: 1024px) {
  .example-class {
    font-size: 24px;
  }
}

/* Output */
.btn {
  background: red;
  font-weight: bold;
}

.btn.is-active {
  opacity: 1;
}

@media (min-width: 1024px) {
  .btn {
    font-size: 24px;
  }
}

.example-class {
  background: red;
}

.example-class {
  font-weight: bold;
}

.example-class.is-active {
  opacity: 1;
}

@media (min-width: 1024px) {
  .example-class {
    font-size: 24px;
  }
}

This makes it possible to @apply the container class for example:

/* Input */
.custom-class {
  @apply container;
}

/* Output */
.custom-class {
  width: 100%;
}
@media (min-width: 640px) {
  .custom-class {
    max-width: 640px;
  }
}
@media (min-width: 768px) {
  .custom-class {
    max-width: 768px;
  }
}
@media (min-width: 1024px) {
  .custom-class {
    max-width: 1024px;
  }
}
@media (min-width: 1280px) {
  .custom-class {
    max-width: 1280px;
  }
}

Recursive @apply

The @apply directive can now be used recursively

Detailed explanation of behavior

This new implementation is designed around a single guiding principle:

Copying a list of classes from your HTML, pasting them after @apply, then replacing the list of classes with the new class should result in the exact same observed behavior as keeping the list of classes in your HTML.

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:

<style>
  .link {
    color: black;
    font-weight: bold;
  }
  .link.is-active {
    background: yellow;
  }
</style>

<a href="#" class="link text-lg leading-7">Link</a>

If we wanted to extract a new class here, we would write this CSS:

.link-lg {
  @apply link text-lg leading-7;
}

Now we can replace the class list like so:

<a href="#" class="link-lg">Link</a>

Now consider what happens if in the original example, we add the is-active class:

<a href="#" class="link text-lg leading-7 is-active">Link</a>

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:

<a href="#" class="link-lg is-active">Link</a>

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:

.link {
  color: black;
  font-weight: bold;
}
.link.is-active {
  background: yellow;
}

.link-lg {
  @apply link text-lg leading-7;
}

...we generate this output:

.link {
  color: black;
  font-weight: bold;
}
.link.is-active {
  background: yellow;
}
.link-lg {
  color: black;
  font-weight: bold;
  font-size: 1.125rem;
  leading: 1.75rem;
}
.link-lg.is-active {
  background: yellow;
}

Applied classes follow CSS source order, not apply order

Consider this example:

<div class="bg-white bg-black">
  <!-- ... -->
</div>
<div class="bg-black bg-white">
  <!-- ... -->
</div>

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:

.foo {
  @apply bg-white bg-black;
}
.bar {
  @apply bg-white bg-black;
}

...needs to generate this output:

.foo {
  background-color: black;
  background-color: white;
}
.bar {
  background-color: black;
  background-color: white;
}

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 no flex 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:

.pt-4 {
  background-color: lol;
}
.foo {
  background-color: lol;
}

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 default pt-4 unless it can't find pt-4 in your own CSS.

In this PR, you'd get this output:

.pt-4 {
  background-color: lol;
}
.foo {
  padding-top: 1rem;
  background-color: lol;
}

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:

.foo {
  background-color: red;
  @apply text-white font-normal hover:font-bold;
  text-align: center;
}

This PR generates this output:

.foo {
  background-color: red;
  color: white;
  font-weight: normal;
}
.foo:hover {
  font-weight: bold;
}
.foo {
  text-align: center;
}

Seem a little confusing? Think about it in expanded form:

.foo {
  background-color: red;
}

/* Start of `@apply`'d classes */
.foo {
  color: white;
  font-weight: normal;
}
.foo:hover {
  font-weight: bold;
}

/* End of `@apply`'d classes */
.foo {
  text-align: center;
}

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:

/* Input */
.foo {
  @apply bg-white bg-black;
}
.bar {
  @apply bg-black bg-white;
}

/* Output */
.foo {
  background-color: white;
  background-color: black;
}
.bar {
  background-color: black;
  background-color: white;
}

With this PR, things will work like this:

/* 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;
}

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:

.foo {
  @apply p-4 pt-2;
}

...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:

.foo {
  @apply tw-mt-6;
}

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:

.foo {
  @apply .bg-white;
}

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 removed

There used to be a draft for an @apply rule that worked with these custom property bag things like this:

.foo {
  @apply --custom-thing;
}

Prior to this PR, Tailwind supported mixing that functionality with Tailwind's version of @apply, so you could do this:

/* Input */
.foo {
  @apply --custom-thing text-center;
}

/* Output */
.foo {
  @apply --custom-thing;
  text-align: center;
}

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:

/* Input */
.foo {
  @apply font-normal hover:font-bold text-white hover:text-black;
}

/* Output */
.foo {
  font-weight: normal;
}
.foo:hover {
  font-weight: bold;
}
.foo {
  color: white;
}
.foo:hover {
  color: black;
}

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:

// tailwind.config.js
module.exports = {
  experimental: {
    applyComplexClasses: true,
  },
}

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.

@adamwathan adamwathan changed the title [WIP] Add initial support for applying variants and other complex classes Add support for @apply with complex classes, including responsive and pseudo-class variants Aug 14, 2020
@mgibas
Copy link

mgibas commented Aug 14, 2020

This looks great 🔥🚀

@aashiqahmed98
Copy link

aashiqahmed98 commented Aug 14, 2020

Just today thought it'll be nice if hover resides inside the @apply

@adamwathan

@hivokas
Copy link
Contributor

hivokas commented Aug 14, 2020

This is the new era of Tailwind 🍃

@benface
Copy link
Contributor

benface commented Aug 15, 2020

This PR is a work of art. 👏

With this PR, things will work like this:

/* 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 @apply on two lines?

.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;
}

@adamwathan
Copy link
Member Author

@benface Yep you are correct! You can use multiple apply rules as a sort of escape hatch 👍🏻

@adamwathan adamwathan merged commit 8a066c2 into master Aug 15, 2020
@adamwathan adamwathan deleted the apply-complex-classes branch August 15, 2020 22:08
Copy link

@LionsAd LionsAd left a 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;
Copy link

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?

Copy link
Member Author

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

Copy link

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.

@agcty
Copy link

agcty commented Aug 18, 2020

When will this be released?

Setting

experimental: {
    applyComplexClasses: true,
},

with tailwind 1.6.2 doesn't do anything atm

@jeffchown
Copy link

jeffchown commented Aug 19, 2020

This is one of my favourite new features - Thanks!

Just tried it and all works as expected except for use with the disabled: variant. Is there anything I can do on my end to enable this feature for disabled: as well?

UPDATE: e.g. The error I receive on npm run dev is The disabled:bg-gray-300 class does not exist...

SOLVED: when I explicitly added backgroundColor: ['hover', 'focus', 'active', 'disabled'], to the variants section in my tailwind.config.js then this feature worked as expected - I thought backgroundColor already had those variants activated by default.

@mikemand
Copy link

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-red no longer overrides the colors of .button:

<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?

@adamwathan
Copy link
Member Author

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.

@mikemand
Copy link

Sure, no problem.

@mikemand
Copy link

Nevermind, I figured it out. I have another class for dashboard buttons that is applying my button class. When compiling, the default button's styles are being pushed down with the dashboard button styles, so now the default button styles are below the other colored button styles. If I don't apply button in my dashboard button, all is well. I'll just copy out the styles I need (everything except padding and font size) and it should be fixed. Sorry for the confusion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Feature Request/Discussion] @apply with media queries and pseudo selectors
9 participants