[Feature Proposal] CSS class override utility for component composition. #1446
Replies: 27 comments 32 replies
-
I would very much like to see something like this. I'm not sure if it makes sense for it to be part of the main codebase though. |
Beta Was this translation helpful? Give feedback.
-
Probably doesn't make sense in the main codebase, but I wanted to raise the issue here to get more feedback. I tried my hand at understanding the tailwind full config and developing something for this. It turned out to be a lot more complicated than I was initially expecting. I'm happy to do the work and create an NPM module for it, but I'll definitely need some guidance on how to get the full computed config via JavaScript and how to get the class string groups and match them with config groupings to do the merge. |
Beta Was this translation helpful? Give feedback.
-
This is a pretty complex problem and probably out of scope for Tailwind itself unfortunately.
^ This of course is the really hard part and I can't even provide guidance on a good approach for doing it :/ One day I'd like to explore a proper CSS-in-JS flavor of Tailwind that would handle stuff like this better but I'm going to close this for now as a "not working on this any time soon" sort of thing unfortunately. If you do end up hacking on it as a separate project I'd love to see how it goes 👍 |
Beta Was this translation helpful? Give feedback.
-
I created a really simple and naive helper for this but there are a number of utilities it doesn't work with at all. The approach is to simply split each classname by the final instance of The proper solution would be to iterate over each classname, figure out which utility it belonged to, and then make sure each utility only has one value. However, like @harryhorton I have no idea how to actually begin doing that. The only other alternative would be to tweak your own tailwind config so that no two utilities have the same prefix (e.g. replace Anyway, here's the little helper I created, it is still useful as long as you only stick to utilities that don't share prefixes, but it's probably just as likely to drive you insane when you can't figure out why you can't set text size and text colour at the same time. Again, just to be clear to anyone who stumbles into this issue: This snippet not a good solution! Don't start using it without understanding why it will probably break your code
If there was a future breaking change in a major release that gave each utility its own prefix then this would be trivial but I can't imagine there's very much appetite from the overall user-base for that. |
Beta Was this translation helpful? Give feedback.
-
@SeanRoberts looks cool! Thanks for sharing! I tried creating utilities too, but there was complicated to try to cover all the use cases. I ended up using https://github.com/ben-rogerson/twin.macro Not as good for minimal css, but great for the kind of flexibility I needed. By compiling to CSS, you can do simple overrides and easily use tailwind classes via props. I start with classes only, and move to twin macro when I need more flexibility. |
Beta Was this translation helpful? Give feedback.
-
@harryhorton Oh cool, I hadn't heard of this before! What approach do you use for the problem we're talking about in this issue? Does it just handle merging classnames automatically? If you do |
Beta Was this translation helpful? Give feedback.
-
@SeanRoberts My preferred approach looks like this: If I'm not doing anything programmatic with tailwind classes const Thing: FC = ()=>(<div className="bg-red-500" />) If I have ANY conditional logic, I'll take this approach: import tw from "twin.macro";
import { ButtonProps } from "./types";
import styled from "styled-components";
export const StyledButton = styled.button<ButtonProps>(
// using the styled components array form makes it really easy to conditionally include styles.
({ flat, fullWidth, small, text, disabled }) => [
// this is the base/default
tw`text-white rounded px-6 py-2 bg-blue-600 shadow`,
// these are conditional overrides and additions
fullWidth && tw`w-full`,
small && tw`text-sm px-5 py-1`,
flat && tw`bg-transparent shadow-none`,
text && tw`p-0 bg-transparent shadow-none`,
disabled && tw`opacity-75`,
]
); Passing values like nubmers or colors is a little wonky as I'm pretty sure twin-macro will complain if anything but a complete tailwind class is output, but I haven't had to do that yet. One of the benefits of this approach is that it makes it easy to mix with regular CSS using styled components. With overrides, anything that comes after will override the first just like css (because that's what it copiles to). Another benefit is that you can use Styled components to compose styles together using tailwind. Something you can't easily do with just classes and requires overrides. Hope that helps! |
Beta Was this translation helpful? Give feedback.
-
@harryhorton Awesome, thanks for sharing! Hopefully at some point we'll have the tools to be able to just merge the classNames rather than having to generate more CSS with a macro but this definitely solves the problem for me and probably anyone else who finds this issue in the meantime. |
Beta Was this translation helpful? Give feedback.
-
I think that mapping of classname to grouping is really going to the key to having a utility that works perfectly for this. Especially because classes are dynamically generated, so one team might use classnames like "text-14 text-16 text-18" and another might use "text-sm text-lg text-xl". Splitting by hyphens also completely falls apart when you get into classes like "bg-red-500" or "text-extra-big" I think it would either need to be part of the build step, or a separate tool that reads your config file and generates the manifest. |
Beta Was this translation helpful? Give feedback.
-
Not promising anything, but I've spent the last couple of hours playing with options and I'm now busy manually capturing everything into a map... |
Beta Was this translation helpful? Give feedback.
-
I captured all the class names manually and put it in a package: https://github.com/mariusmarais/tailwind-cascade @harryhorton @SeanRoberts, would you mind taking a look to see if this covers your use cases? Working for me so far, but I haven't really made anything very complex with it yet. It currently uses a build-time generated config, but could be modified to read |
Beta Was this translation helpful? Give feedback.
-
@mariusmarais This looks great! I think it'll work in the vast majority of cases, thanks for putting all the work in. I don't think we'll be able to have something bulletproof until there's a way to get access to the actual config object, but this is really close. |
Beta Was this translation helpful? Give feedback.
-
Another alternative would be to generate utilities with lower/higher specificity than the normal ones, which you can use when you want to override something or allow something to be overridden. You could for example generate /* generated CSS */
.base\:text-red { color: red }
.base\:text-blue { color: blue }
.text-red { color: red }
.text-blue { color: blue } const Text = ({ className, children }) => (
<p className={`base:text-red' ${className}`}>{children}</p>
)
<Text className="text-blue">I am blue, da ba dee, da ba di</p> This is sort of similar to the props-based approach @harryhorton proposed as you'll still have to explicitly declare which style props should be overridable by consumers and which shouldn't (not a bad idea anyway imo), but you wouldn't have to deal with two different style APIs (style props and tw classes) across the codebase anymore. You could also do the opposite, generate utilities that appear after the normal ones. This feels similar to /* generated CSS */
.text-red { color: red }
.text-blue { color: blue }
.text-red\! { color: red }
.text-blue\! { color: blue } const Text = ({ className, children }) => (
<p className={`text-red ${className}`}>{children}</p>
)
<Text className="text-blue!">I am blue, da ba dee, da ba di</Text> However, the downside of both of these approaches is that they only allow to increase specificity by one level (two if you combine base, normal and important), so you can override once but after that you're SOL. Should be fine for most cases but not as flexible as CSS-in-JS libs, so worth pointing out. Anyway, not sure if this could be done via a Tailwind plugin without having to copy-paste Tailwind core? 😅 |
Beta Was this translation helpful? Give feedback.
-
Yes, I went down that route initially, building the CSS twice, first normally, then with the I think a JS function that can compute overrides is the best way to solve this problem. I think my experimental package works quite well, and has handled everything I've thrown at it. However, its major weakness is still the grouping of Tailwind classes. I don't really see a way out unless Tailwind generates the / a grouping file as part of the build. I've looked at the Tailwind source, and it looks like it wouldn't be too difficult to add a grouping specifier for the base utilities, but the onus would be on every plugin to opt-in by specifying a grouping, which won't happen unless the overriding is a core feature of Tailwind. One could expose something in the config to provide grouping info as a stop-gap for users, but ultimately it wouldn't work without critical mass of plugins opting in. The reason for the opt-in requirement is simple: because there's no naming convention enforced, there is no way to know which classes are in conflict without the author providing that information, which I think is actually a good thing. Well-designed Tailwind plugins will need to give thought to how their pieces compose with the base in any case. I think the alternative of parsing the generated CSS in JS, and essentially having a mirror implementation of CSS rules to determine overrides would be terrible. If this idea appeals to you, just go with your preferred CSS-in-JS solution, it will work much better than building a CSS-in-JS lib into Tailwind. |
Beta Was this translation helpful? Give feedback.
-
You could create special css classes for your components in tailwind
For
|
Beta Was this translation helpful? Give feedback.
-
Untested, but I suppose you could create a "last" breakpoint in your tailwind config:
Then |
Beta Was this translation helpful? Give feedback.
-
Awhile back I came up with another pure CSS solution: Like @ianjamieson's solution, it only allows for one "level" of override. The difference is that it provides a variant that "underrides" normal class selectivity. So, as a component author, you can set up some "default" colors, etc., and the component user can override them with normal Tailwind classes. |
Beta Was this translation helpful? Give feedback.
-
Just my 2 cents: Most of the problems around needing an override system go away once you accept it's better to just split major visual distinctions of a single component into separate components. And for minor visual distinctions, to just not have the component apply any default version of that class so there's no issue with "override precedence". Being able to tell what something will look like in the browser just by glancing over the markup in your component is one of the strong guarantees of Tailwind- and arguably one of the theses of Atomic CSS in general. Large amounts of conditionals and overrides done purely for the sake of fitting 4 visually distinct versions of a single component into a single |
Beta Was this translation helpful? Give feedback.
-
A better solution now will be to use the JIT compiler and the
|
Beta Was this translation helpful? Give feedback.
-
Hi, everybody if you are using React, you could then use xstyled. Xstyled is a CSS utility library written on top of styled-components and emotion, thereby it makes overriding CSS much easier. The library is good alternative for this stuff. |
Beta Was this translation helpful? Give feedback.
-
this is my solution using the classnames library to render classes conditionally:
here gets a bit messy with things like padding, but does a good job overall. |
Beta Was this translation helpful? Give feedback.
-
Hey @harryhorton! I think I solved the issue you have here → https://github.com/dcastil/tailwind-merge. The main difference is that the merge function is called |
Beta Was this translation helpful? Give feedback.
-
Perhaps now that Tailwind v3 is out and looking great it's a good time to revisit this idea? I think the benefits of this approach could be huge, specifically this thread's idea where styles (including atomic classes) are scoped to components like their surrounding JS, and then styles and classes outside can override them. I think it makes a lot of sense. The props approach kind of conflicts with the philosophy of avoiding naming styles for things, and it harder to read then creating some base component and then wrapping it in another component which add styles or modifies existing styles. |
Beta Was this translation helpful? Give feedback.
-
Here's my solution, as a plugin: https://github.com/richardtallent/tailwindcss-def This adds a "def:" variant you can use in a component to specify TailwindCSS classes you want to be default styles. Any class using this variant can be overridden on a component instance with normal TailwindCSS classes. Example: <!-- MyButton.vue -->
<template>
<button class="font-semibold def:bg-blue-500"><slot/></button>
</template> 4. Override as needed for your component instances<MyButton>My Blue Button</MyButton>
<MyButton class="bg-red-500">My Red Button</MyButton> This only gives you one "level" of override before you have to resort to other means to get more specificity, and doesn't work in Safari 13 and below, but otherwise, it's a simple, no-JS-required solution. And I have to mention, this is my first plugin, the Tailwind CSS plugin and variant architecture is very easy to work with! |
Beta Was this translation helpful? Give feedback.
-
Here's an approach very similar to @richardtallent's idea but adds https://play.tailwindcss.com/1j10rEZpsq?file=config Downside is the specificity is too low to defeat component styles still, so not totally perfect. Personally I think the right solution for this sort of thing is a prop on the component for controlling this: function MyComponent({ radius = 'default' }) {
let radiusClass = {
default: 'rounded',
full: 'rounded-full'
}[radius]
return (
<div className={`${radiusClass} ...`}>
{/* ... */}
</div>
)
}
<MyComponent radius="full" /> That's how we've always handled this in our own projects 👍🏻 |
Beta Was this translation helpful? Give feedback.
-
Hey all, another approach I've found to this problem that seems promising (if you use Vite) is to simply not using vanilla Tailwind. Instead, I'm using UnoCSS, another atomic CSS library by antfu which I discovered recently that has full support for Tailwind, but with a lot more too. There's a bunch of great things it does that I won't mentioned here since it would be off-topic, but the relevant feature for this thread is that when using the Vite plugin you can scope the generated classes using a few different strategies:
This doesn't necessarily directly address the issue of the CSS cascade, but so far from my testing it seems that this strategy—combined with the different way that UnoCSS generates the classes when compared with vanilla Tailwind—resolves the issue. I'd be interested to hear if you all have the same experience. |
Beta Was this translation helpful? Give feedback.
-
I've managed to solve some of the problems others have run into and have created a Tailwind plugin, tailwind-unimportant, that creates class variants that can be used in components. Features:
E.g.: // text-blue takes precedence over -:text-green, which takes precedence over --:text-red
<a class="--:text-red -:text-green text-blue">
The link is blue
</a> |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
First, ❤️ Tailwind!!!
Problem
Working within the scope of a large FE application (my use case is React with hundreds of components), it's common to create styled components that are composable. Reading through the doc notes on composability, the standard answer seems to be to split the utilities that COULD change into their own classes (or in our case, components that would merge the class list with the base).
While that seems like it could work, in our situation, splitting things up this way doesn't feel reliable as we often receive adjustments to styles that can wildly change the look of a component that we're composing off of. It would result in having a lot of base components with next to no utility classes, and a lot of additional utilities on a hierarchy of composing components that feels like it would be difficult to manage. Additionally, if render functions or props were used to override all of the classes, then a developer would easily loose track of duplicate utilities across compositions, where they should have been simply extending the base.
It's possible to work within the limitations of the current recommendation, but, after testing, I don't expect this to be a good developer experience at scale.
Possible solution
Create a JS utility that can merge utility classes based on what can be overriden.
This would allow developers to write base common styles on a usable component, while easily using component composition to override only the utilties that they wish to change.
tailwindMerge('bg-gray-500 p-4 mb-3', 'bg-blue-500 pr-2 mb-4') => 'p-4 bg-blue-500 pr-2 mb-3'
React
This would also work with other front-end frameworks.
Additional considerations.
Beta Was this translation helpful? Give feedback.
All reactions