Replies: 13 comments 63 replies
-
There have been other suggestions about adding such a feature. The maintainers have addressed this request before, that you may wish to read up on if you have not already done so: |
Beta Was this translation helpful? Give feedback.
-
Thanks to @wongjn, I was able to create a small plugin to address this issue: |
Beta Was this translation helpful? Give feedback.
-
Sorry, I'm not following. I'm using Tailwind, because it makes sense in the source code :) What I would disagree on with them, would perhaps their premise.
What's the argument here? Did I say typing "hover:" a couple of times hurts? It may? It may not? The app may have crosshair menus in the future, it may not?... |
Beta Was this translation helpful? Give feedback.
-
@mybearworld @sarimarton @adamwathan I was thinking that we can enjoy both worlds by creating a vite plugin of sorts where you can transpile the grouped syntax into a plain tailwind utility selector (modifier) just before tailwind gets to read your code: import { parse } from "@babel/parser";
import * as t from "@babel/types";
import generate from "@babel/generator";
import { Plugin } from "vite";
export const groupModifierPlugin = ({ enforce } = {}): Plugin => {
return {
name: "vite-tailwindcss-group-modifier",
enforce,
load() {
console.log("plugin loaded");
},
transform(code: string, id: string) {
if (!id.endsWith(".tsx")) return code;
const ast = parse(code, {
sourceType: "module",
plugins: ["jsx", "typescript"],
});
// console.log(t.traverse(ast, { enter: console.log }));
t.traverse(ast, {
enter(path) {
if (path.type !== "JSXAttribute" && path.name?.name !== "className")
return;
const valueNode = path.value as t.StringLiteral;
if (!valueNode) return;
let newClassNames = valueNode.value.replace(
/(\w+:(?:\w+:)*)x-\[([^]*?)\]/g,
(match, prefixes, p1) => {
const prefix = prefixes.slice(0, -1).split(/\):\(/);
const classes = p1.split(",").map((c) => c.trim());
return classes
.map((c) => {
return prefix.concat(c).join(":");
})
.join(" ");
},
);
valueNode.value = newClassNames;
},
});
return generate(ast).code;
},
} as Plugin;
}; So essentially, it converts to the plain syntax AOT and you can have all the things. I was able to implement a POC using safelist since I wasn't able to get in before tailwind did for some reason and didn't put too much time into that as I wanted to know whether you think its something feasible? |
Beta Was this translation helpful? Give feedback.
-
In the project I'm working on currently, there's no repetition at all:
results in .print-shadow .print-shadow\:apply-\[w-\[calc\(50\%-16px\)\]\2c inline-block\2c m-2\] {
display: inline-block;
width: calc(50% - 16px);
margin:1rem;
} This is exactly what I want. plugins: [
plugin(function ({ addVariant }) {
addVariant('print-shadow', '.print-shadow &')
}),
// Make grouping variants work
// https://github.com/tailwindlabs/tailwindcss/discussions/11701#discussioncomment-6569866
plugin(({ matchUtilities }) => {
matchUtilities({
apply: (value) => {
return {
[`@apply ${value
.replace(/,/g, ' ')
.replace(/ - /g, '-')
}`]: {}
}
}
})
})
] |
Beta Was this translation helpful? Give feedback.
-
Why would it be bad? Do you know what a red herring is?
It didn't get any better...
Lol. Pixel-optimized on the breakpoints. Why would I use pixel values for paddings? The media query values are highly context specific, specific to the fix number of tabs, their wordings etc, which might change upon requirement. In that case, it makes sense to adjust the pixel values (and keep it adjustable by using pixel values), and not figure out how to play with pre-set breakpoints.
Strawman. Don't do this.
While you're ignoring my counterarguments. Don't do this.
You're overengineering. |
Beta Was this translation helpful? Give feedback.
-
I've ended up just doing the following in js since I'm already using clsx and tailwind-merge to process tailwind classes:
|
Beta Was this translation helpful? Give feedback.
-
Hi. Has there been any decisions on this? I believe variants grouping is a great improvement. Seems like there's already a working version here #11701 (reply in thread) |
Beta Was this translation helpful? Give feedback.
-
This seems to be the most active thread discussing modifier and selector groupings, so I'm joining the discussion here after closing a new thread and being pointed here by @wongjn (thanks). I've been catching up with the discussion both here and on Twitter on performance implications of groupings and some of the feedback from the maintainers on issues with relying on runtimes or potential need for source-code manipulation. As this seems to be a much requested feature, and we would love to see something that combines that amazing power of new support for Let's take for example the classes applied in the blog post announcing support for <label class="has-[:checked]:ring-indigo-500 has-[:checked]:text-indigo-900 has-[:checked]:bg-indigo-50 ..">
<svg fill="currentColor">
<!-- ... -->
</svg>
Google Pay
<input type="radio" class="accent-indigo-500 ..." />
</label> This component's classes are already getting a little unruly, but the goal is simple - apply a set of classes when the same set of modifier conditions are met:
The first route is to apply separate classes, which results in the following code generated: .has-\[\:checked\]\:bg-indigo-50:has(:checked){
--tw-bg-opacity: 1;
background-color: rgb(238 242 255 / var(--tw-bg-opacity))
}
.has-\[\:checked\]\:text-indigo-900:has(:checked){
--tw-text-opacity: 1;
color: rgb(49 46 129 / var(--tw-text-opacity))
}
.has-\[\:checked\]\:ring-indigo-500:has(:checked){
--tw-ring-opacity: 1;
--tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity))
} Another way to achieve the same result is to use .has-ring {
@apply has-[:checked]:ring-indigo-500 has-[:checked]:text-indigo-900 has-[:checked]:bg-indigo-50;
} The resulting code from this .has-ring:has(:checked){
--tw-bg-opacity: 1;
background-color: rgb(238 242 255 / var(--tw-bg-opacity));
--tw-text-opacity: 1;
color: rgb(49 46 129 / var(--tw-text-opacity));
--tw-ring-opacity: 1;
--tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity))
} As you can see it is exactly the same. Here is the Tailwind Play sketch with that same label styled both ways. I understand that a more general implementation of grouping, as described by Adam on the Twitter thread, will result in redundant code from selector multiplication, but a restricted use-case, for example one where chaining is not allowed after grouping, would already afford huge DX benefits. A simple example would be for the same rules (assuming a <label class="has-[:checked]:(ring-indigo-500,text-indigo-900,bg-indigo-50)" /> Wouldn't this essentially be syntactic sugar for the same This is illustrated manually in the Tailwind Play sketch by defining this named class: .has-\[\:checked\]\:\(\ring-indigo-500\,\text-indigo-900\,\bg-indigo-50\)\ {
@apply has-[:checked]:ring-indigo-500 has-[:checked]:text-indigo-900 has-[:checked]:bg-indigo-50;
} I understand there are many other caveats when a feature like this is integrated into the larger project, but as I said I think even a limited implementation with reasonable restrictions, like requiring groupings are the last operators, to prevent exponential code generation would be a massive readability and DX boost. While this is possible through extensions, native support would surely be more performant, less prone to issues and easier to integrate with IDE tooling such as autocompletion and conflict detection. |
Beta Was this translation helpful? Give feedback.
-
Now that TailwindCSS is being largely overhauled with version 4.0, does that change anything about the arguments against including this as a built-in feature within TailwindCSS? Because as many have pointed out before. It would greatly reduce the visual noise when reading code and help make things more DRY which I also believe would help make things more maintainable. |
Beta Was this translation helpful? Give feedback.
-
so, after reading this thread, which has been going on for two years, I've come to a couple of conclusions. And the problems are as I realised only in what the group will be compiled into, but also I wanted to point out the UX component of using such groups and their approaches. Here's a list of plugins that people have made and suggestions, with my discussion: As I understand it, this is one of the oldest and most popular works on this topic, because it even touched on changes to a certain version of tailwindcss. <div class="hover:multi-['bg-red-500;text-white']">...</div> In my opinion this is a totally messy and awkward syntax, in addition to having to write an extra utility (multi-), you also have to write About the implementation internally: <input type="text" value="tbone" disabled class="mt-1 block w-full px-3 py-2 bg-white border border-slate-300
rounded-md text-sm shadow-sm placeholder-slate-400
focus:x-[outline-none,border-sky-500,ring-1,ring-sky-500]
disabled:x-[bg-slate-50,text-slate-500,border-slate-200,shadow-none]
invalid:x-[border-pink-500,text-pink-600]
focus:invalid:-x[border-pink-500,ring-pink-500]
"/> A better option. Without I like this idea too and it epitomises the point. Inside: as I understand, also made on the basis of What is the problem So it will be something like this: .apply-[...] {
@apply ...
}
.apply-[...] {
@apply ...
} If I have misunderstood how tailwindcss works in this situation - please comment for me
A good option, on par with content.transform This is not a good way and considering that not for every cn() function is set. import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs) {
return twMerge(twGroups(clsx(inputs)));
}
function twGroups(input) {
const pattern = /([\w-]+):\(([^)]+)\)/g;
return input.replace(pattern, (_, prefix, classes) => {
return classes.split(/\s+/)
.map((c) => `${prefix}:${c}`)
.join(' ');
});
}
In my opinion this is the best realisation. Syntax: convenient, nothing superfluous Before: <div className={'relative z-10 pb-8 bg-white sm:pb-16 md:pb-20 lg:(max-w-2xl,w-full,pb-2) xl:pb-32'}>
{children}
</div> After: <div className="relative z-10 pb-8 bg-white sm:pb-16 md:pb-20 lg:max-w-2xl lg:w-full lg:pb-2 xl:pb-32">
{children}
</div> The only thing: P.S About the ‘what it will compile into’ issue - what some people want it to be as a single class after compilation - this is really expensive (will grow every time), so the best solution for now is to stick with the option that tailwind suggests |
Beta Was this translation helpful? Give feedback.
-
I tried to make my own plugins to support several classes and this is what happened: Unfortunately there are issues with the tailwindcss ecosystem, specifically the plugins for eslint and intelliSense, but maybe someone could use it I tried to change the prettier extension for tailwindcss, but it didn't run, but maybe someone will need some work on it. const regExp = /(?:(^|["'`]|\s))((\w+?):\((.+)\))(?:(["'`]|\s|$))/g
function splitClasses (classStr = '') {
const matches = [];
const classStrWith$ = classStr.replaceAll(regExp, (match, p1, p2, p3, p4 = '', p5, offset) => {
matches.push(p2);
return p1 + `$${matches.length - 1}` + p5;
});
console.log(matches)
const splitedClasses = classStrWith$.split(/([\t\r\f\n\s]+)/);
console.log("splited:", splitedClasses);
const splitedClassesWithout$ = splitedClasses.map((item,index) => {
if(item[0] === "$") {
const indexMatche = item[1];
return matches[indexMatche];
}
return item;
})
return splitedClassesWithout$;
}
const str = "text-yellow mm:(text-greenHaze bg-redOrange)"
let parts = splitClasses(str)
let classes = parts.filter((_, i) => i % 2 === 0)
let whitespace = parts.filter((_, i) => i % 2 !== 0)
console.log(classes);
console.log(whitespace) |
Beta Was this translation helpful? Give feedback.
-
I wrote this plugin that helps me a lot for the nature of grouping responsive modifiers: https://www.npmjs.com/package/vite-plugin-twneat It probably has bugs and not very performant, but the code is fairly straightforward - it just concats things and dumps it in a safelist. I am not sure but I don't think it produces more classes than what you would have written natively without groupings. PS: I am a backend developer who started writing some simple landing page code and got overwhelmed with the entire javascript ecosystem. So apologies if I don't understand what is going on. |
Beta Was this translation helpful? Give feedback.
-
Given the following code
We can see that there's a multiple repetition of each modifier (
:focus
,:disabled
,:invalid
) as the syntax only allows passing a single class for each one.My suggestion is to group the classes under a single modifier to make it more DRY in the following (or a similar) manner:
This, in theory, could also enable modifier chaining + class grouping in the following manner:
It is, in some senses, similar to what can be achieved using scss where a modifier can be nested within a block of previously declared modifiers:
Beta Was this translation helpful? Give feedback.
All reactions