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

RFC: 3.2.6 should be a major release as it includes breaking changes regarding overriding class names #10603

Closed
jdpt0 opened this issue Feb 16, 2023 · 2 comments

Comments

@jdpt0
Copy link

jdpt0 commented Feb 16, 2023

#10552 and #10543 are examples of this.

This comes down to the fact that although to some it's considered a bug, many people (myself included) use it as a feature and override the tailwind CSS classes, expecting the CSS class to be overwritten with the latest value specified. Since this functionality has been changed (there has been no indication of the change other than these issues) between these versions many of our components are now rendering in a completely different layout between the two versions.

For example, I may have a Card component with a default of p-4 but I may want to override that to p-2 in some circumstances. On version 3.2.4 I was able to just use something like classnames (https://www.npmjs.com/package/classnames) to merge the strings together on the base component and the later value would take precedence. In order to do that now on 3.2.6, I have to use something like tw-merge (https://www.npmjs.com/package/tailwind-merge) to make this happen, which removes the previously defined tailwind values.

While it seems on the surface like this is just a fix to a minor bug, it can have a big impact on web apps that have many abstracted components with overriding tailwind classes, which is why I believe this change should result in a major release rather than a patch.

@adamwathan
Copy link
Member

adamwathan commented Feb 16, 2023

Hey @jdpowell1! So to really explain this I need to explain how Tailwind sorts CSS rules generally, which is a lot of information so bear with me — will try to make it as digestible as possible.

First, every class in Tailwind comes from some sort of internal plugin, and each plugin usually maps to a CSS plugin. For example, there's a display plugin, a backgroundColor plugin, a fontSize plugin, etc.

We maintain a list of these plugins internally in a particular order, and all of the generated CSS follows this order, so that all of the classes handled by a single plugin are grouped together, and those groups are sorted according to the plugin order.

So given this HTML for example:

<div class="flex bg-red-500 text-sm">
  <div class="inline-block bg-blue-500 text-xl">
    <!-- ... -->
  </div>
</div>

...the following CSS is generated:

/* `display` utilities */
.inline-block {
  display: inline-block;
}

.flex {
  display: flex;
}

/* `background-color` utilities */
.bg-blue-500 {
  --tw-bg-opacity: 1;
  background-color: rgb(59 130 246 / var(--tw-bg-opacity));
}

.bg-red-500 {
  --tw-bg-opacity: 1;
  background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}

/* `font-size` utilities */
.text-sm {
  font-size: 0.875rem;
  line-height: 1.25rem;
}

.text-xl {
  font-size: 1.25rem;
  line-height: 1.75rem;
}

Now in addition to this sorting logic, we also do some sorting within certain plugins to make sure that more specific utilities from a particular property category override more general utilities.

An example of this is that pl-3 will always be sorted later in the stylesheet than px-5, which itself is sorted later than p-6.

That ensures that things like this work:

<div class="p-5 pr-4">

Beyond that level though, in Tailwind CSS v3.2.4 and earlier, the generated CSS is sorted based on the order Tailwind "sees" the class names in your templates.

That means that the sort order for any two utilities that were part of the same "category" was non-deterministic.

So for example, given this HTML:

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

...Tailwind would scan it for classes, find bg-black and bg-white, and generate the CSS for those classes in that order, resulting in CSS that looked like this:

.bg-black {
  background-color: #000;
}

.bg-white {
  background-color: #fff;
}

But given this HTML:

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

...the generated classes would be in the opposite order:

.bg-white {
  background-color: #fff;
}

.bg-black {
  background-color: #000;
}

...and because a class name will only ever appear in the final CSS once but can appear in templates many times, this ordering is based just on the first time Tailwind sees each class.

This means that any time you change the HTML in an existing file, or add a new file that contains utility classes, or even compile your CSS on a different computer that happens to give Tailwind the list of files from the filesystem in a different order that the resulting CSS could have slight differences in the order of "conflicting" classes.

Historically we have considered this a non-issue and avoided making this deterministic to maximize performance, because we have always considered it user error to add two competing classes to the same element:

<div class="bg-black bg-white">
  Should I be black or white?
</div>

Our IntelliSense extension marks this as a lint warning for example:

image

So even though the behavior has changed for v3.2.5+, adding two conflicting classes to the same element has always been an authoring error in our minds.


Okay so all of that explained, I understand that even with an explanation it still feels like a frustrating breaking change.

The reality is though any project that was doing this was already at risk of breaking any time you changed something in another file.

You gave this example:

For example, I may have a Card component with a default of p-4 but I may want to override that to p-2 in some circumstances. On version 3.2.4 I was able to just use something like classnames (https://www.npmjs.com/package/classnames) to merge the strings together on the base component and the later value would take precedence.

This isn't actually true — it might work that way in your project right now, but if you ever add p-2 to another file in your project that is scanned before the Card file, this would instantly break.

Here are some demos that shows that, running v3.2.4:

Demo 1, where p-2 overrides p-4
Demo 2, where p-2 does not override p-4

You should be able to replicate this in your own project by just adding p-2 even as a comment somewhere near the top of your Card file.

Also, if overriding p-4 with p-2 works in your project right now, that means overriding p-2 with p-4 does not work right, because only one of them can work.

In CSS, the order of the classes in your CSS determines which rules are applied, not the order of the classes in your HTML.

That means that in this example, both elements will be black:

<style>
.bg-white {
  background-color: #fff;
}

.bg-black {
  background-color: #000;
}
</style>

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

So again even though the output has changed a bit for v3.2.5, the behavior you were seeing in v3.2.4 and earlier was non-deterministic, and overrides that worked one day could break the following day if someone used one of those classes in another file in a way that caused Tailwind to ingest the class names in a different order. This is trivial to trigger, as demonstrated by the demos above.


Now in v3.2.5 and up, the generated CSS order is always deterministic, but even so we still highly discourage adding conflicting classes to the same element and still consider it an authoring mistake. The generated CSS will at least always be the same going forward, so the precedence of classes won't surprisingly change just because you compiled the CSS on a different computer, but because the order of the class names in the class attribute has no effect on which class actually wins, I still think it's a bad idea because you have no ability to control which class actually wins at the template level. If you want to override bg-white with bg-black in one file but do the reverse in another file, you simply can't do it.


I hope that helps — again the key thing to understand here is that if you were doing this sort of overriding in v3.2.4 and earlier, that behavior could break at any time just by adding a class in a different file. So it might feel like this is a breaking change but really the project was already "broken" because there was no guarantee that the next time you compiled your CSS that you were going to get the same output that you got the previous time.

Put another way, p-2 might be overriding p-4 in your project right now, but in someone else's project p-4 was overriding p-2, and a tiny change to your own project could cause that behavior to flip at any time too.

So even if we reverted the change that was made here, your project would still be at risk of breaking any time you made a change to it.

Sorry for the long-winded response but hopefully it includes all of the detail necessary to really understand what's happening here!

@jdpt0
Copy link
Author

jdpt0 commented Feb 16, 2023

Hi @adamwathan, thanks for the very detailed response, it's really helped me understand the problem you're facing deep at the CSS level. Is this intricacy explained in the documentation? I may have glossed over it!

I'm glad I've found tailwind-merge, which should help us to achieve the same result in a semantically correct, deterministic way!

Thanks for all you and the Tailwind team have done, you've really transformed our workflow around styling our components.

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

No branches or pull requests

2 participants