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

[Feature Request/Discussion] @apply with media queries and pseudo selectors #313

Closed
paulcsmith opened this issue Dec 13, 2017 · 23 comments · Fixed by #2159
Closed

[Feature Request/Discussion] @apply with media queries and pseudo selectors #313

paulcsmith opened this issue Dec 13, 2017 · 23 comments · Fixed by #2159

Comments

@paulcsmith
Copy link

@apply currently does not work with pseudo selectors and media queries. I can see why that is difficult, but I wonder if it is on the roadmap to support something like that in the future?

I'd love it if @apply could make media queries or hover selectors automatically. Maybe it would have to be a Saas/Less only feature because I'm not sure how it would work with raw CSS, but I'm not sure.

Thanks for considering, and thanks for making such an awesome lib

@01ivr3
Copy link

01ivr3 commented Jan 22, 2018

Amen. Is this possible?

@adamwathan
Copy link
Member

Would like to support this eventually but it's not simple at all; probably at least a full week of effort to figure out and implement. One day, but no timeline right now.

@paulcsmith
Copy link
Author

Yeah I can imagine it's a lot of work! Thanks for the response and congrats on making such an incredible project 👍

@sebastiandedeyne
Copy link
Contributor

I'd love to see this in Tailwind some time but I understand the implementation can be a can of worms. Is there anything that can be contributed to help with this, or is it something that needs to be done by the core team?

@hacknug hacknug mentioned this issue Jan 30, 2018
Closed
@abebraham
Copy link

I don't know if this helps anyone but this worked for me using scss instead of css.

.default_class {
  @apply .border-grey;
  &:hover {
    @apply .border-grey-lighter;
  }
}

@MattRogowski
Copy link

I think this is along the same lines of what @paulcsmith was after, I had hoped something like this was possible:

h1 {
    @apply text-size-3 lg:text-size-5;
}

I know it starts to go against the concept of it being utility driven but if some elements are truly globally styled it'd be great to not have to add the same classes to every instance of the element, and also not have to write custom media queries for a couple of attributes.

@adamwathan
Copy link
Member

Hey @MattRogowski! That's actually totally possible already, you just need to use media queries or our @screen directive:

h1 {
  @apply .text-size-3;
  @screen lg {
    @apply .text-size-5;
  }
}

Eventually I'd like to make it possible to @apply responsive utilities directly but it's a hard feature to build and since you can solve that problem by just applying non-responsive utilities into media queries, I've been focusing my efforts on other more important problems that currently have no solution.

@adamwathan
Copy link
Member

Just wanted to leave some notes for myself here as I start thinking about how the fuck this could possibly work...

Possible approaches

  1. Look up a match by having variant plugins provide a function for reverse engineering the non-variant name

In this scenario, when someone types @apply hover:bg-blue, Tailwind would first look for hover:bg-blue and not find it (because that's not actually the selector, the selector is hover:bg-blue:hover). After that, we'd fallback to another lookup method (similar to how shadow lookup works) where we would iterate over all of the variant plugins asking "hey, is this a utility you would generate?", and if the variant plugin says "yes", the variant plugin would provide us with the rule we need to append after the rule that contained @apply.

The biggest challenge with this is getting the order right. If someone type @apply focus:bg-blue hover:bg-red but according to their module options, hover variants should appear before focus variants, we need to make sure we maintain that order in the resulting CSS. This will probably be hard. Same problem for responsive variants, we don't want to render large variants before small variants just because someone said @apply lg:block sm:inline.

The shittiest thing about this solution is that all variant plugins would need to provide this "reverse engineering" function to deduce if a class would have been generated by that plugin. That's a shitty burden for plugin authors.

  1. Look for the real match in the actual CSS

In this scenario, we would use postcss-selector-parser during the lookup to make sure hover:bg-blue does match hover:bg-blue:hover, by stripping the pseudo selector before doing the match. This has a shitty performance penalty we would have to work around because we couldn't do a hash lookup on this anymore, we'd literally have to iterate and check as we go unless we precompiled a new lookup map, which is probably possible but would need to be figured out.

Using this approach, we could also keep track of the matches position in the stylesheet so that when we go to append the matched rules we can preserve the order in which they appeared in the original stylesheet. This part is easier using this option, but actually finding a match is harder and more expensive.

Overall this feature is going to be very hard to do :)

@benface
Copy link
Contributor

benface commented Sep 22, 2018

@adamwathan Correct me if I’m wrong, but I see a problem with approach #1. What if a plugin defines a new variant, let’s say group-active:, and another plugin defines a new utility, let’s say text-shadow. Which plugin would be responsible for reverse-engineering the class group-active:text-shadow?

@adamwathan
Copy link
Member

@benface It would be the variant plugin's responsibility, so you'd pass group-active:text-shadow to the plugin and it would have to determine whether or not it looked like a variant that that plugin would generate.

Overall I think that approach is shitty — the whole idea of having to break it apart sucks, because it's theoretically possible to write a variant plugin that makes it impossible to reverse engineer the original parts so I think it's fundamentally flawed.

Thinking on it more I think what I will need to do is something more like option 2, but where we build our own data structure in advance for doing the lookups and have it include all of Tailwind's utilities + generated variants, and when you look them up there the result should include any of the necessary meta data, like the priority of the variant and stuff like that. I think it's doable but there is going to be complexity around multi-class selectors (like .group .group-hover:...) for sure.

Variant plugins might need to report a "lookup key" for each generated rule or something, so like this rule:

.group:hover .group-hover\:bg-blue { ... }

...would need to report a lookup key of "group-hover:bg-blue" since that is how people are going to try and "apply" that. I hope I don't need to do that but not sure it'll be possible to avoid.

@adamwathan
Copy link
Member

Related to this whole thing, I think part of making this work might mean removing support for applying your own custom utilities if they are defined in CSS. You'd still be able to do it but you'd need to define them in plugins. The reason I'm guessing this might be a problem is because I think it's going to be hard to pull the meta data we need out of the CSS vs. having the JS data structure in advance, but I might be wrong, we'll see.

@adamwathan
Copy link
Member

Another idea, it might be easier to implement all this stuff if internally we use a nested syntax for our own CSS. It would certainly help for focus/hover/etc, but maybe not for responsive stuff since we group all of our responsive declarations.

Example:

.hover\:bg-blue {
 &:hover {
    background-color: blue
  }
}

Now the lookup key is still just hover:bg-blue, and when we naively just duplicate the contents of that rule into the rule using apply, it'll pull in the &:hover which will then get expanded when the CSS is processed.

Would need to actually try it to know for sure if there's other crap to worry about but might make some things easier.

This approach works in Less too.

@adamwathan
Copy link
Member

The biggest challenge with this is getting the order right. If someone type @apply focus:bg-blue hover:bg-red but according to their module options, hover variants should appear before focus variants, we need to make sure we maintain that order in the resulting CSS. This will probably be hard. Same problem for responsive variants, we don't want to render large variants before small variants just because someone said @apply lg:block sm:inline.

Quoting myself here but thinking about this more, it's probably not that crazy if the apply order actually matter, because that's currently the case anyways.

Consider this HTML:

<div class="pr-5 p-2">...</div>

Because pr-5 is defined later than p-2 in Tailwind's utilities, it will override the right padding defined by the p-2 utility.

Now consider extracting those classes to a component class:

.foo {
  @apply pr-5 p-2;
}

In this case, Tailwind currently renders this output:

.foo {
  padding-right: 1.25rem;
  padding: .5rem;
}

...which means the padding-right declaration actually does nothing, because padding overrides it.

I don't know if we should consider this a bug or not necessarily, but it is the current behavior so I think it's fine to ignore the order problem for a first pass at making it possible to apply utility variants directly.

@spiltcoffee
Copy link
Contributor

I don't know if we should consider this a bug or not necessarily, but it is the current behavior so I think it's fine to ignore the order problem for a first pass at making it possible to apply utility variants directly.

I think I've thought of this as a feature when using @apply, so this definitely makes sense at first.

It is inconsistent, though, if @apply is intended to mimic a HTML element class attribute, since the order of classes doesn't matter in HTML but does with @apply.

In the end, dunno. I think it depends on what you intended for @apply to achieve as to whether this is a bug.

@adamwathan
Copy link
Member

Another idea, it might be easier to implement all this stuff if internally we use a nested syntax for our own CSS.

Following up, I think this is going to be the path forward.

For all this stuff to work in a sane way, we have to stop thinking of utilities as classes and instead think of them as mixins/functions. As part of Tailwind's processing, it just happens to create a class for every single utility function so that you can use those utility functions in your HTML.

From an implementation perspective, Tailwind is going to need to augment the shadow lookup table to include utility variants (right now it just includes raw utilities), so we'd have something like this:

const shadowLookup = {
  'bg-blue': {},
  'hover:bg-blue': {
    '&:hover': {
      // ...
    }
  },
  'group-hover:bg-pink': {
    '.group:hover &': {
      // ...
    }
  },
  // ...
  'sm:bg-blue': {
    '@media ...': {
      // ...
    }
  },
  'sm:hover:bg-blue': {
    '@media ...': {
      '&:hover': {
        // ...
      }
    }
  },
  // ...
}

This way, when someone tries to @apply a class like sm:hover:bg-blue, we get to do a direct lookup, and pull out this set of properties:

{
  '@media (min-width: 576px)': {
    '&:hover': {
      backgroundColor: 'blue',
    }
  }
}

Then we just copy that set of properties into the component where @apply is being used, and process the entire resulting CSS tree with postcss-nesting.

One consequence of this approach that I think I'm okay with is that nesting is now a "feature" of Tailwind, because there's no really straightforward way for us to process nesting for Tailwind-generated stuff and not for user generated stuff. This increases the scope of Tailwind a bit and I would prefer if it could be avoided, but I don't think it can.

There's also an issue of postcss-nesting dropping support for nesting media queries: csstools/postcss-nesting#24

This is the right thing to do for that project since it's not part of any spec, but since we would need to have that working for this feature to work, we'll have to write our own plugin that just handles nested media queries and have that run before postcss-nesting runs.

Probably tackle this in December.

@joshistoast
Copy link

An easy workaround looks something like this:

.site-nav {
    @screen md {
        @apply flex-row
    }
    @apply flex pt-10
}

By using the @screen you can specify what breakpoint and apply styles accordingly.

@paulcsmith
Copy link
Author

Thanks @joshwcorbett. I think that's a totally acceptable solution!

@rightaway
Copy link

@joshwcorbett Any workaround for hover styles?

@adamwathan
Copy link
Member

@rightaway As per the docs:

/* Won't work: */
.btn {
  @apply block bg-red-500;
  @apply hover:bg-blue-500;
  @apply md:inline-block;
}

/* Do this instead: */
.btn {
  @apply block bg-red-500;
}
.btn:hover {
  @apply bg-blue-500;
}
@screen md {
  .btn {
    @apply inline-block;
  }
}

@joshistoast
Copy link

@rightaway I usually use a poscss plugin called ‘precss’ which gives me the ability to use SASS-like syntax. So when using this plugin, I can use the ‘&:hover’.

I’m typing this on iPad so I can’t do a code snippet but I hope this helps.

@anibalsanchez
Copy link

For Tailwind 2.0, it would be great to have something like @screen for all pseudo selectors.

.my-button {
    @apply bg-blue-500;

    @hover {
        @apply bg-blue-700 text-white;
    }

    @active {
        @apply bg-blue-900 text-white;
    }
}

.my-input {
    @apply bg-blue-500;

    @focus {
        @apply bg-white border-blue-400;
    }
}

@LionsAd
Copy link

LionsAd commented Aug 7, 2020

Here is an action plan:

  • The first thing to do to reduce your support calls is to make your “compiler” smart:

Rust is freakishly complicated, but the compiler gives so great explanations that you can just follow it and it all works

So instead of leaving this unfixed you do:

  • When hover:[] is encountered in apply you throw and error and tell the user that they should use an explicit:

rule:hover {
@apply(...)
}

  • When md:, xs: etc is encountered you point to screen

I understand you want to discourage @apply, but still need to keep it, but also don’t want to spend 100s of Support hours.

In that case not fixing this issue magically (which could have side effects), but instead leading the user to the solution should solve the problem.

I love all the tailwind magic, but it is not necessary for everything.

For those that don’t want to do it manually and are okay with magic side effects, they could still use the magic prefix apply plugin.

So it’s a win for everyone - even if the feature request is ultimatively declined that way.

@anibalsanchez
Copy link

From my view, Tailwind is a language, so @apply and @screen are declarations that help the expressiveness. In that direction, @hover, @active, etc. would be new additions in the course of empowering the Tailwind language.

In the other hand, I understand that Tailwind is "only" a utility-first framework, a set of handy CSS classes. So, it is going to be only a layer of convenient CSS classes. In that line, as Adam pointed out from docs, that this is the best lean approach that is currently supported:

.btn {
  @apply block bg-red-500;
}
.btn:hover {
  @apply bg-blue-500;
}
@screen md {
  .btn {
    @apply inline-block;
  }
}

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