Skip to content

Add @silent at-rule #169

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

Closed
wants to merge 4 commits into from
Closed

Add @silent at-rule #169

wants to merge 4 commits into from

Conversation

adamwathan
Copy link
Member

I'm not convinced I want to merge this but opening for discussion.

This PR adds support for this syntax:

@silent {
    .foo {
        color: blue;
    }
}

.bar {
    @apply .foo;
}

...which would generate:

.bar {
    color: blue;
}

This lets you do things like this:

@silent {
    @tailwind utilities;

    /* Necessary to make sure responsive variations are silenced. */
    @tailwind screens;
}

.btn {
    @apply .bg-blue;
    @apply .text-white;
    @apply .font-semibold;
    @apply .px-4 py-2;
}

The goal is to try and resolve situations like #150, where people want to use @apply to reference utility classes inside of scoped style blocks like in a Vue component.

If anyone in that situation wants to pull this branch down and give it a shot, I'd be interested in knowing your feedback.

Just because it's insanely complex to support it in any weird nested
ways and I'd rather people get an error for now instead of bizarre
unexpected results.
@syropian
Copy link

syropian commented Nov 9, 2017

I'm all for this. I understand with Tailwind you're trying to push utility classes in HTML over slapping @apply in a bunch of CSS, but this keeps Tailwind flexible regardless. At the end of the day the most important thing Tailwind does for me is create a consistent design system. That's my number 1 usage. Being able to seamlessly use it across regular css files and something like Vue single-file components is a huge bonus for me.

@reinink
Copy link
Member

reinink commented Nov 9, 2017

Adam, I am once again torn on whether or not this should be done as a custom at-rule, or simply done in the Tailwind config. Considering some of the config ideas we've been bouncing around lately, I wonder if we should do something like this:

{
  options: {
    silent: true,
  }
}

@reinink
Copy link
Member

reinink commented Nov 9, 2017

Expanding on my previous comment, my fear is that just because we've got this power to create custom at-rules in PostCSS, does that mean we should for everything? Having some obviously makes sense (apply, responsive, tailwind), but I wonder how wise it is to create tons of these. Using custom at-rules seems way more likely to create future compatibly issues over simply managing this in our own JS config file.

@adamwathan
Copy link
Member Author

adamwathan commented Nov 9, 2017 via email

@reinink
Copy link
Member

reinink commented Nov 9, 2017

Yeah real good points. I'd love to explore this a little more as well:

I’d also be curious to understand more about the general styling architecture when building a SPA with lots of CSS in JS; do you still have a global stylesheet at all? If not I wonder how well any CSS framework really fits into that story?

Like, why build a CSS component if you're already building a JS component? What's the point?

@adamwathan
Copy link
Member Author

Like, why build a CSS component if you're already building a JS component? What's the point?

I'm trying to understand this too; I'd love to see an example of a real Vue component where it feels necessary to use @apply instead of using utilities directly.

@bskimball
Copy link

bskimball commented Nov 10, 2017

For me the point is limiting the bundle sizes. The css for tailwinds is 170kb (obviously less if gzipped), but I may not need that for initial load. I like using nuxt for frontend, which makes it easy to control what gets downloaded and when.

I like the consistency of using a system like tailwind. Which I think may be better than what I'm currently doing. My current set up is having a config file with CSS4 vars, I import that file in to every vue component that needs style and use the vars in that component. It allows complete control over the download size while maintaining a consistent config.

I think using tailwind in a similar set up would be nice,
Example:

// BaseContainer.vue
<template>
    <div>
        <slot />
    </div>
</template>
<style scoped>
    div {
        @apply .container .mx-auto
    }
</style>

@syropian
Copy link

@adamwathan Extracting lots of components is a good practice but I find myself dreading having to create custom components for things like buttons, text fields, and other form fields that don't add any additional functionality on top of the native element. Sometimes it's nice to have a class for consistently styling them without fully encapsulating them within a component.

@adamwathan
Copy link
Member Author

@syropian I'm confused; why can't you just create a .btn component in your CSS then? It seems like recreating the same .btn CSS class over and over in every Vue component is a real waste of effort?

Where are you defining .btn such that it can be used by multiple Vue components if not just in a CSS file?

@syropian
Copy link

@adamwathan For example, my login/register screens may have certain styles of inputs and buttons that differ than the main app screens. I could create these styles in a global stylesheet of course, but I prefer the encapsulation of keeping them in the components. It's about trying to find a balance of "keeping styles encapsulated" without over-encapsulating my creating a new Vue component for every single little element.

@adamwathan
Copy link
Member Author

@syropian I get that but why do you need to use @apply? What is the benefit of doing this:

<template>
  ...
</template>

<script>
  ...
</script>

<style>
.btn {
    @apply .bg-blue .text-white .etc;
}
</style>

...if you can't even reuse that .btn class outside of this individual file?

I guess there's the situation where you have one component with two input fields that look the same; in that case could you just use a computed property instead?

<template>
  <div>
    <input :class="[inputStyles]">
    <input :class="[inputStyles]">
  </div>
</template>

<script>
  export default {
    // ...
    computed: {
        inputStyles() {
            return 'appearance-none px-4 py-2 border ...'
        }
    }
  }
</script>

Personally I would still just do this:

<template>
  <div>
    <input class="appearance-none px-4 py-2 border ...">
    <input class="appearance-none px-4 py-2 border ...">
  </div>
</template>

<script>
  export default {
    // ...
  }
</script>

I guess I just don't understand the real benefit to composing CSS classes that can only be reused in the same file.

I can understand the desire for it but I'm struggling to justify the effort it would take to support it in a sane way 😕

@syropian
Copy link

There's definitely no shortage of different ways to accomplish it, this is just my ideal method. I don't love the idea using computed props for pure presentational purposes personally.

All this being said this isn't make or break for me. I'd like this feature but if it gets added or not, I would still keep using Tailwind :)

@adamwathan
Copy link
Member Author

adamwathan commented Nov 10, 2017

Alright so me and @reinink just talked about this for about an hour and a half, so here's a summary of our thoughts on it all.

Goals

  1. To be able to use @apply to reference utility classes from inside a scope where those utility classes don't explicitly exist, such as the <style> block of a Vue component.

  2. To be able to use Tailwind's utilities through @apply alone, using Tailwind only as a way to constrain design decisions. When using Tailwind this way, actually adding Tailwind's utility classes to your outputted CSS is unnecessary and considered a problem.

(If we're misunderstanding the goals here, please let us know!)

Solution #1: @silent

Allow rendering Tailwind's utilities "silently", such that they do exist (at least temporarily) in a Vue component's <style> block, but are stripped after @apply is finished with them.

Pros

  • It solves both problems in a fairly composable way; you can easily control what styles should be actually rendered and which should just be available as temporary references
  • It's not a lot of code
  • We already have a proof of concept

Cons

  • There's a slightly annoying maintainability issue that creeps in with this code, and that's that it is the first plugin in our processing chain that has caused other plugins to have to specifically be aware of it. The findMixin function now needs to explicitly check if a class is directly nested under @silent, whereas before it could operate in it's own little world without knowing about the rest of the plugin chain. This is a shitty precedent.
  • It is pretty lame to have to add @silent { @tailwind utilities; @tailwind screens; } to the top of every Vue style block
  • I really worry about the performance of basically re-running Tailwind from scratch for every single Vue component in your project. I worry this will make the development experience slow and painful. There's also a chance this isn't a problem at all.
  • If performance is a problem, I'll be on the hook to try and fix it because people will complain. This will be annoying because this is a feature I don't even personally care about.
  • You can't use @apply with custom utilities unless you duplicate those custom utilities into every <style> block inside of a @silent rule.

Solution #2: Fallback to a phantom node tree if a utility isn't matched

This is what is outlined more or less in #150. The general idea is Tailwind would maintain a list of "utilities I would generate if asked to generate utilities", and if a class is used with @apply but Tailwind can't find it, it would fallback to this phantom map to see if it exists there and use that instead.

If it can't be found in the phantom node tree, you'd get an error like you do now.

Pros

  • Solves both problems without introducing a new at-rule or forcing you to add new code to your <style> blocks; things would "just work"
  • Probably easier to optimize performance, because it becomes an explicit feature whereas with @silent, Tailwind itself was essentially blind to your actual goals; it would just generate utilities whenever it was asked. That is a pro in that the code is simple, but a con in that it's harder to optimize.

Cons

  • Unknown effort; this could be super hard, no idea what challenges we'll hit here yet
  • You still can't use @apply with custom utilities

Our Stance

Overall we see the benefit of making this work, especially for goal #1. Goal #2 we don't care about as much; Tailwind is a utility framework and if that's not how you want to work, we can't justify investing a lot of time into making it perfect for some other workflow, especially when we have so much to do still to optimize for the workflow it was designed for.

The main problems for us are:

  1. We have so many other things we want to work on that are higher priority because they are things we personally want out of the framework.

    That includes things like:

    • Adding missing utilities (accessibility helpers, focus features, outline utilities, table style utilities)
    • Finishing the documentation and adding more fleshed out examples
    • Working on our extension and theming system to make it easy to pull in pre-built components like buttons, forms, etc.
  2. Some of the things we want to work on are going to affect the overall architecture of the current codebase. If supporting this feature ends up being complex, it's going to make it harder for us to add what we want to add next.

    We'd much rather add that stuff first, then re-evaluate what it looks like to solve this problem again. I have a feeling it's going to make it easier to solve.

So with that said, I'm going to close this PR for now. We are interested in continuing to discuss this problem, but we don't have any intention to solve it until some higher priority stuff is finished.

I'm going to keep this branch around in case anyone (like @syropian) want to pull it down and try it out with their Vue projects because I'm curious about how it works out performance-wise and how well it feels like it solves the problem, but overall I'm expecting solution #2 to be a better approach.

Another thing I'd encourage people to explore is whether or not you can configure things with Webpack such that Tailwind runs just once; against your entire combined set of styles, instead of against each <style> block individually. If that's possible, we might be able to just forget about this problem entirely.

@bskimball
Copy link

@adamwathan Thanks for the detailed response! You guys are awesome, and I love the work you guys are doing. I will probably play with this branch a bit and see what happens.

@DaftMonk
Copy link

DaftMonk commented Nov 12, 2017

I personally would love @apply to behave similarly to scss mixins, or perhaps an alternative keyword that does. You can import entire mixins file in scss, and none of it gets included in your final bundle, except for the mixin styles that are applied to your class.

I'm using Vue and Nuxt, which gives some powerful code splitting right out of the box. If @apply worked like mixins, I would be able to avoid pulling in the entire tailwind css generated file, and could instead just mixin tailwind CSS into my components where needed, benefiting from a reduced initial load time, while getting the consistency that tailwind provides.

@01ivr3
Copy link

01ivr3 commented Dec 21, 2017

Just chiming in on this closed issue 😜 , I agree it could be a powerful feature if @apply could be used to pull in Tailwind's utility classes without having to first render out all of it's utility classes.

This definitely would be optimum for using Tailwind with component based UI frameworks like React and Vue.

It could also be handy for controlling file size, instead of looking at adding Purge CSS to the ends of our build tooling and cross our fingers this doesn't break anything.

Not at all saying something like supporting this workflow is more important than mission critical things like adding missing utilities, better fleshing out the docs, or building out an extension and theming system! All of those sound awesome! 👍

But if you guys do come up with an optimum solution while you're chipping away on those, there definitely seems to be a desire, and it may help with issues like the following:

nuxt/nuxt#2273, Integrating TailwindCSS into Nuxt SFC
nuxt/nuxt#2094, tailwind example is misleading
tailwindlabs/discuss#24 (comment)

Apologies, I have little else to contribute other than chiming in from the peanut gallery. My understanding of how Postcss plugins work and the limits of what you can do with them is close to non existent at the moment.

@viglucci
Copy link

viglucci commented Dec 21, 2017

Also adding my 2 cents that this would be a great feature to have.

I love the flexibility we gain with a utility-first pattern, however, being able to inherit from existing classes and styles is an extremely popular and powerful feature of modern preprocessor languages such as less and sass, that I think a lot of the development community has become used to and reliant upon them.

In my specific use case, I would like to be able to do something like the following:

// components/alert/alert.css

.alert {
    @apply .border-t-4;
    @apply .rounded-b;
    @apply .px-4;
    @apply .py-3;
    @apply .shadow-md;
}

.alert--info {
    @apply .bg-teal-lightest;
    @apply .border-teal;
    @apply .text-teal-darkest;
}
// core.css

@tailwind preflight;
@tailwind utilities;

@import 'base.css';
@import '../components/alert/alert.css';
// components/alert/alert.js

import React from 'react';
import classnames from 'classnames';
import PropTypes from 'prop-types';

class Alert extends React.Component {
    render() {
        return (
            <div className={classnames(
                'alert',
                'alert--info',
                this.props.className
            )} role='alert'>
                <div className='flex'>
                    <div className='py-1'><svg className='fill-current h-6 w-6 text-teal mr-4' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'><path d='M2.93 17.07A10 10 0 1 1 17.07 2.93 10 10 0 0 1 2.93 17.07zm12.73-1.41A8 8 0 1 0 4.34 4.34a8 8 0 0 0 11.32 11.32zM9 11V9h2v6H9v-4zm0-6h2v2H9V5z' /></svg></div>
                    <div>
                        <p className='font-bold'>Our privacy policy has changed</p>
                        <p className='text-sm'>Make sure you know how these changes affect you.</p>
                    </div>
                </div>
            </div>
        );
    }
}

Alert.propTypes = {
    className: PropTypes.string
};

export default Alert;

When learning that tailwind would be implemented solely in CSS rather than less or sass, not being able to extend from tailwind classes was definitely one of my major concerns.

The same as @01ivr3, I'm not able to offer much more than a +1 on this request, but hopefully this comment helps you prioritize this feature.

Thanks

@adamwathan
Copy link
Member Author

@viglucci what you are asking for actually already works exactly like you want. It’s only trying to use @apply inside of completely different CSS scopes like in a Vue component’s style block where it currently doesn’t work.

@viglucci
Copy link

@adamwathan is it supposed to work without using the postcss import plugin?

I was receiving the below error until i found your comment here that mentioned the import plugin.

(2:5) @apply cannot be used with .border-t-4 because .border-t-4 either cannot be found, or it's actual definition includes a pseudo-selector like :hover, :active, etc. If you're sure that .border-t-4 exists, make sure that any @import statements are being properly processed before Tailwind CSS sees your CSS, as @apply can only be used for classes in the same CSS tree.

@adamwathan
Copy link
Member Author

adamwathan commented Dec 21, 2017 via email

@jojobyte
Copy link

jojobyte commented Jan 17, 2018

Found Tailwind a week or so ago and was super excited because I thought it would solve this problem because of the docs on extracting components. This is actually a deciding feature in my book and not just a nice to have. I am trying to use Tailwind in Vue single file components.

At the risk of sounding dramatic, without this feature I see little to no difference between Bootstrap, Bulma or Tailwind or any other CSS framework beyond aesthetics.

And to try to pull the sting out of the above statement, let me clarify, without this the only way to use Tailwind the way I need it to work is by importing it through a LESS reference, and if I'm doing that why not just use Bulma or Bootstrap?

@import (less,reference) "bootstrap";
@import (less,reference) "bulma";
@import (less,reference) "tailwind";

Been trying to get @tailwind preflight; to work with the postcss-reference plugin so that postcss could be used throughout the project without needing LESS or SCSS, however it wont work, and most likely because of the stuff described in this issue.

<style lang="postcss">
@reference {
  // @tailwind preflight;
  // @tailwind utilities;
  @import "tailwind/preflight";
  @import "tailwind/utilities";
}
.nav {
  @apply .flex .items-center .justify-between .flex-wrap .bg-orange-dark .p-6;
}
</style>

I've been messing with Webpack/Postcss/Tailwind config files every night for over a week now trying to figure this out, only stumbling across this issue today.

Less won't extend the responsive classes like lg:mt-0, which means using tailwind becomes much less useful.

tl;dr

All frameworks should include a feature like this, its a necessity for how they are used in SPA and to build optimized websites.

@kamalkhan
Copy link

kamalkhan commented Jan 17, 2018

As @adamwathan pointed out here and to avoid repeating the same styles over and over again as the template gets bigger, this is how i am using it in .vue files and it feels nice for me.

<template>
    <div :class="tailwind.card">
        <p :class="tailwind.header">I am a card</p>
    </div>
</template>

<script>
    export default {
        computed: {
            tailwind() {
                return {
                    card: 'p-8 bg-white shadow rounded',
                    header: 'text-gray-dark font-bold',
                }
            }
        }
    }
</script>

@jojobyte
Copy link

@kamalkhan presumably that only works if you're including the global stylesheet somewhere?

Guess I'm looking for a better balance of enabling my laziness without sacrificing performance. 😜

I want to be able to reference anything in the framework while developing, without needing to include the entire framework globally, and have the final result be concise.

I want to have my cake and eat it to. Right now the 🍰 is a lie.

@adamwathan
Copy link
Member Author

I see little to no difference between Bootstrap, Bulma or Tailwind or any other CSS framework beyond aesthetics.

This is a bit of a ridiculous statement considering the entire approach to CSS architecture is different among these tools. You choose Tailwind to take a utility-first approach to building custom user interfaces; you choose Bootstrap if you want predesigned components you can piece together.

Less won't extend the responsive classes like lg:mt-0

Less won't be able to extend any of Tailwind's classes because they aren't generated until after Less has finished running. Regardless, the documentation is clear about how you should handle these situations:

image

presumably that only works if you're including the global stylesheet somewhere?

Yes, this is how Tailwind is intended to be used.

I want to be able to reference anything in the framework while developing, without needing to include the entire framework globally, and have the final result be concise.

Use a tool for removing unused CSS like PurgeCSS.

Right now the 🍰 is a lie.

Not sure what this means; Tailwind doesn't make any promises about this workflow. You are meant to use Tailwind's classes in the class attribute of your HTML elements. The @apply feature is a tool for dealing with duplication when it becomes painful; you're not supposed to use it to create one-off classes that are never re-used. The documentation is clear about this as well.

As mentioned in earlier comments, we agree it would be nice for @apply to be able to reference Tailwind classes even if they don't exist in the current CSS tree, but it's extremely low on the priority list behind a lot of other important things we need to get done.

It's also loaded with hard decisions and complexity, because right now @apply works even for your own custom defined classes, not just the classes Tailwind generates. Should @apply in component A be able to reference a custom class defined in component B's style block?

That said though, the "only use Tailwind's classes in @apply statements and never actually use them in your markup" workflow will never, ever, ever, be important to to us. It's contradictory to the best practices and workflows we encourage that prompted us to create the framework, and we want to build the best framework we can for the workflow we think is best.

If we can make changes down the road that make that workflow possible without any negative consequences against the workflow we actually endorse, then sure, but we're much more focused on making the recommended workflow better than devoting time to enabling a workflow we think is bad.

Workflow aside; as far as I understand it creating one-off classes for every single component is actually bad for performance. The browser is faster at applying the same class to many elements than it is at parsing many classes that contain duplicate declarations and applying those one-off classes once or twice.

Put another way, applying .bg-green to 100 DOM elements renders faster than adding background-color: green to 100 different classes and using each of those classes once.

@jojobyte
Copy link

I apologize, wasn't intending to rile anyone up.

The extracting components feature/docs is very similar to what I want to do, related to Semantic Remapping (which is my cake), my intention was to throw my hat in the ring of support for this feature/issue, and explain my reasoning for it.

Had this issue not existed, I would have gone on my merry way and continue my search of a framework/solution that fits my workflow.

While I'm sure I could use a refresher in best practices, my goal is not have 100 background-color: green; references, but using the styles and base a framework provides to build class components that make sense for each project.

For example a component that styles form field blocks

.field {
  @apply .mb-4;
  & > label {
    @apply .hidden .select-none .uppercase .text-grey-dark .text-xs .font-bold .mb-2;
  }
  & > input {
    @apply .appearance-none .w-full .py-2 .px-1 .border-0 .border-b-2 .border-blue-lighter .bg-transparent;
    &::placeholder {
      @apply .text-grey-dark;
    }
    &:focus {
      @apply .shadow .border-blue;
      &::placeholder {
        @apply .text-grey-darkest;
      }
    }
  }
  & > .error {
    @apply .hidden .text-red-light .text-xs .italic .py-2 .px-1;
  }
}
<!-- 
this is much more in line with my goals as there will be 100+ form fields easily 
being able to update all of them with a tiny CSS tweak would be ideal
-->
<div class="field">
  <label for="foo">Name</label>
  <input id="foo" placeholder="John" v-model.trim="name">
</div>

<!-- 
vs
-->
<div class="mb-4">
  <label class="hidden select-none uppercase text-grey-dark text-xs font-bold mb-2" for="foo">Name</label>
  <input class="appearance-none w-full py-2 px-1 border-0 border-b-2 border-blue-lighter bg-transparent" id="foo" placeholder="John" v-model.trim="name">
</div>

That CSS might eventually be extracted out into a global style rather than stored in a Vue <field> component or some such, thereby making best use of css, but it wouldnt need to include every bit of the CSS framework its not using, nor would you need to trust that PurgeCSS stripped out and optimized everything you intended.

@adamwathan
Copy link
Member Author

Totally hear you; the funny thing is I used to be a big advocate of writing CSS that way a few years ago, here's a blog post for example:

http://transmission.vehikl.com/using-bootstrap-as-a-semantic-mixin-library/
(scroll to the final section)

These days I disagree with everything I recommend in that article 😄 I wrote about what convinced me to take a different approach here if you're interested:

https://adamwathan.me/css-utility-classes-and-separation-of-concerns/

That post basically outlines all of the motivations behind creating Tailwind in the first place 👍

Anyways, I am still interested in making this work eventually, it's just mega low priority because I will personally never use the framework that way, and this being something I built for myself that I can only dedicate a few hours a week to in my spare time, I'm naturally going to prioritize the stuff that's most helpful to me and best aligned with what I see being the vision for the framework.

@jarrodldavis
Copy link

The best way I've found to handle repetitive class lists in Vue is to use CSS modules. They are easy to use in Single File Components and there's a way to do composition, even from the global CSS scope.

So, something like this:

<template>
  <div>
    <p :class="$style.myParagraph">Paragraph 1</p>
    <p :class="$style.myParagraph">Paragraph 2</p>
    <p :class="$style.myParagraph">Paragraph 3</p>
  </div>
</template>

<script>
export default {
  name: 'Example'
}
</script>

<style lang="postcss" module>
.myParagraph {
  composes: m-2 p-2 border border-grey text-blue from global;
}
</style>

will output the following HTML:

<div>
  <p class="Example_myParagraph__1Qr6s_0 m-2 p-2 border border-grey text-blue">Paragraph 1</p> 
  <p class="Example_myParagraph__1Qr6s_0 m-2 p-2 border border-grey text-blue">Paragraph 2</p> 
  <p class="Example_myParagraph__1Qr6s_0 m-2 p-2 border border-grey text-blue">Paragraph 3</p>
</div>

and the following empty ruleset:

<style type="text/css">
.Example_myParagraph__1Qr6s_0 {
}
</style>

which could easily be optimized away with another PostCSS plugin.

Here's a full runnable example.

@tpavlek
Copy link

tpavlek commented Mar 14, 2018

Hey @adamwathan I know that this is closed and I appreciate your comments about higher priority things, but I have what I think might be a "legitimate" use-case for this.

I'm using v-autocomplete - it does some magic and creates elements on-the fly when included. I'm just instantiating the component, never touching those smaller divs and inputs.

It's official recommendation for how to style and interact with the app is to style particular classes.

So roughly my component looks something like this:

<template>
<div>
    <span>Type your location</span>
    <v-autocomplete></v-autocomplete>
</div>
</template>

<style>
.v-autocomplete-input {
   @apply .p-2 .border .shadow;
}
</style>

It's not necessarily a high-priority issue because I can just style those classes in my main.css, but ideally I'd like those style definitions to live right beside the only place they're ever used - in this AutoComplete component.

@egorpavlikhin
Copy link

Another use case is when you are restyling external components, where you don't have access to html. The best practice would be to have it in a component with component bound styling (css modules in react, or separate .sass file in angular). The only way to change the styling of an external component would be to override the css classes, but tailwind only allows that for the root stylesheet.

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.