w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.42k stars 647 forks source link

[selectors] New selector to facilitate utility classes (`[name~^=value]`? `.prefix-*`?) #10001

Open LeaVerou opened 5 months ago

LeaVerou commented 5 months ago

(I’d be surprised if this has not been proposed before, but I was not able to find anything)

Pain point

It is very very common to use classes with a shared prefix as to specify key-value pairs. For example:

These utility classes have commonalities shared across all instances, as well as declarations that are specific to the value used. Usually, these are implemented in one of three ways:

  1. Enumeration of all the possibilities in the CSS, when they are finite. Manually (and painfully) when they are few, with build tools when they are dozens. This includes both CSS rules for the specific values of each utility class, as well as exceedingly long selector lists for the commonalities among them.
  2. Pushing the friction to authors by asking them to use one class for the commonalities and one for the specific values. E.g. to use a Bootstrap Icons icon, authors need to use both a bi class and a bi-iconname class.
  3. This is not an option for libraries, but authors (who have more control of their markup) may do things like [class^="prefix-"], [class*=" prefix-"]

None of these are ideal. It seems pretty straightforward to provide the capability these use cases really need, so this seems like a a quick win.

While the vast majority of use cases are around class names, I could see something like this being useful for all attributes that take a space-separated list of values.

Potential solutions

I see two options:

  1. Extending class selectors to support wildcards. It appears that .foo-* is actually not currently valid, so it seems like a natural choice, especially as it extends nicely to other types of wildcard selectors such as element names, attribute names, ids, etc.
  2. Introducing a new attribute selector, that is basically a combination of ~= and ^=. Options for the operator could be ~^=, ^~=, or even ^~ if we want to keep it to a max of 2 characters. This is quicker to implement, but very specific to attribute selectors, and far less intuitive.
bkardell commented 5 months ago

If I understand you are saying that libraries cant do *= because the amount of fuzziness there may have many too potential false positive matches, but the selectors you're suggesting would instead allow fuzzy matching to at least be on the individual dom tokens? If so, I do like that... I may be biased in that I like improving our regexp powers in CSS selectors all around.

Loirooriol commented 5 months ago

It appears that .foo-* is actually not currently valid, so it seems like a natural choice.

I think that can be confusing because * looks like the universal selector (analogous to https://github.com/w3c/csswg-drafts/issues/6571#issuecomment-912144703).

Introducing a new attribute selector, that is basically a combination of ~= and ^=

I would go this way. I imagine a combination of ~= and $= could also be useful (but less frequently).

romainmenke commented 5 months ago

I am strongly in favor of more powerful tools for attribute selectors but I do wonder if the use case is valid.

Encoding a lot of key/value pairs in class names seems like the wrong approach. Is there a reason that frameworks like Tailwind, Bootstrap do not use attributes?

Maybe because attributes are supposed to have a data- prefix, whereas classes are always user defined. The data- prefix makes them much more verbose.

pt-6 -> [data-pt~="6"] <div class="pt-6"> => <div data-pt="6">

Styling the commonalities then becomes trivial with [data-pt]

Crissov commented 5 months ago

There is #1010, but that is about regular expressions and element selectors, not simpler wildcard class selectors, which were also not covered in #354.

For what it’s worth, I believe utility classes for atomic design tokens – i.e. usually with a name that roughly matches its single style rule like .font-sm {font-size: 0.8rem} – are very much an anti-pattern for production-level CSS. They should be a tool for early-stage development only, which are then grouped into patterns. However, I’m probably in the minority with that view nowadays.

That is partially why I suggested, in #3714, to introduce something like this:

$fg-green {color: var(--my-green);}
$bg-white {background: white;}

which could then be applied verbosely in HTML as

<a style="@include $fg-green $bg-white">dev</a>

and if that is found to establish a common pattern, it would be moved back to CSS:

.pattern {@include $fg-green $bg-white;}

and the HTML could become more readable and semantic again.

<a class="pattern">prod</a>

Applied to one of the initial examples:

$bi {/*commonalities*/}
.bi-iconname {
  @include $bi;
  --icon: url(icons.svg#name);
}

instead of the now suggested

.bi-* {/*commonalities*/}
.bi-iconname {
  --icon: url(icons.svg#name);
}
xiaochengh commented 5 months ago

@Crissov This looks like custom mixins (#9350)

So IIUC wildcard class selectors and custom mixins are basically solving the same problem.

I'm also wondering if a selector that targets a group of class names / attribute names will make implementations significantly more complicated. For example, Blink maintains maps keyed by class names for efficient style matching and invalidation, and it will be much more complicated if we have to eg put a trie there for prefix matching.

Mixins solve the same use cases without affecting style matching and invalidation. So +1 to mixins over wildcard selectors.

LeaVerou commented 5 months ago

@romainmenke

I am strongly in favor of more powerful tools for attribute selectors but I do wonder if the use case is valid.

Encoding a lot of key/value pairs in class names seems like the wrong approach. Is there a reason that frameworks like Tailwind, Bootstrap do not use attributes?

Maybe because attributes are supposed to have a data- prefix, whereas classes are always user defined. The data- prefix makes them much more verbose.

pt-6 -> [data-pt~="6"] <div class="pt-6"> => <div data-pt="6">

Styling the commonalities then becomes trivial with [data-pt]

I cannot speak on behalf of every author, but I suspect the reasons are multi-fold:

  1. For certain things, classes feel more semantically appropriate. Basically, ask yourself why would you use classes over data- attributes (providing a value is not the only reason, since you can have boolean attributes!). The exact same reasons apply to these use cases. These are not just key-value pairs, they are opting in to a certain style AND parameterizing it.
  2. Verbosity, yes. Using a data- attribute is 6 extra characters. For something as short as pt-6, that's 175% (!) more reading & typing.
  3. In some cases the parameterization even evolves from a single class name. E.g. columns, with later variants columns-3, columns-4 etc.

The fact that authors are willing to go through the pain of currently using this pattern instead of using data- attributes where they could get this for free is a testament to how important this is to them. So the answer cannot just be you’re doing it wrong.

That said, being able to target attributes based on a prefix of their name is also needed, though less frequently.

@Crissov For some of these use cases, the weight of having to specify the mixin on all of these would produce far more code than having a separate base class. Consider e.g. Bootstrap Icons. They have over 2000 icons. The mixin approach would add 2000 * 13 = 26,000 characters to their CSS code. In contrast, their base .bi class is 4 declarations… And Font Awesome has even more icons…

Crissov commented 5 months ago

@xiaochengh, like I said there, it’s mostly a syntax choice (disregarding benefits for JS).

To really improve the icon use case, you’d probably want something like this instead:

.iconname {
  --icon: src(concat("icons.svg#", selector(classname)));
}
LeaVerou commented 5 months ago

@Crissov having a function to get the part after the class name and string concatenation are both very useful in their own right, but without a way to target elements with a given class prefix where would you apply this? In your code example you use .iconname as the selector, does that mean you envision 2000+ CSS rules repeating the same thing? Or a 2000 selector-long selector list? Or making it a library user problem and continuing to ask users to manage two classes?

Crissov commented 5 months ago

I’d expect a very long list of selectors. Also, attr(class) or something like list(1 from attr(class)) seems more feasible than selector().

Anyway, that’s sidetracking from the original issue, so I should probably raise a new one.

xiaochengh commented 5 months ago

Consider e.g. Bootstrap Icons. They have over 2000 icons. The mixin approach would add 2000 * 13 = 26,000 characters to their CSS code. In contrast, their base .bi class is 4 declarations… And Font Awesome has even more icons…

That's a good argument. I take my opposition back.

This is like an inheritance vs. composition debate. Normally I prefer composition, but in CSS verbosity is a much bigger concern, as more code means more network usage.

LeaVerou commented 5 months ago

In terms of syntax, I just had a discussion with @fantasai about how we actually need to be able to target prefixes across several types of selectors, not just classes:

Therefore, I think using a wildcard, despite its unfortunate similarity with the universal selector, is the way to go here.

romainmenke commented 5 months ago

I think we need to consider other options than the wildcard for elements, classes, Maybe even a functional notation. e.g. :has-prefix(.foo-)

I am concerned about a couple of things:

I think that having the absolute shortest notation for the selector isn't that critical here. If possible without too much friction I think we should pursue this.

Loirooriol commented 5 months ago

I'm not convinced.

element selectors

Perfectly covered by :tag() from https://github.com/w3c/csswg-drafts/issues/6571#issuecomment-912144703

attribute presence

We could add something for this, e.g.

:attr(^= foo-) /* Matches if there is an attribute whose name starts with foo- */
:attr(^= foo-, ^= bar-) /* Matches if there is an attribute whose name starts with foo-
                           and whose value starts with bar- */

By using * we lose the ability to customize the matching, e.g. specifying case sensitivity.

Crissov commented 5 months ago

Would reverse spread syntax work?

.foo-... {bar: baz;}
volkantash commented 5 months ago

Seçicilerde eksik yanlar var iken böyle bir değişiklik ertelenmelidir.

Örneğin aşağıdaki gibi değerleri CSS'nin şimdiki seçicilerini kullanarak daha az kod ile çözemiyoruz.

Translated with Google Translate

Such a change should be postponed while there are shortcomings in the selectors.

For example, we cannot solve values like the ones below with less code using CSS's current selectors.

<div class="ml-10px mr-15px"></div>
[class*="ml-1"]{
--ml-xx = 1;
}
[class*="0px"]{
--ml-x = 0;
}
[class*="5px"]{
--ml-x = 5;
}
[class*="ml-"]{
margin-left: calc( var(--ml-xx) * 10 + var(--ml-x) )px;
}

Olmalı (Veya daha iyisi olmalı)

Translated with Google Translate

Should be (Or better yet should be)

<div class="ml-10px mr-15px"></div>
[class*="ml-1*"]{
--ml-xx = 1;
}
[class*="ml-*0*"]{
--ml-x = 0;
}
[class*="ml-*5*"]{
--ml-x = 5;
}
[class*="ml-**px"]{
margin-left: calc( var(--ml-xx) * 10 + var(--ml-x) )px;
}

Not: yukarıdaki örnekte 0-99 arası değerler yaklaşık 20 satır kod ile işletilebilir. Bununla ilgili bir yenilik gelmese de kendimce çözümlerim var. ="ml10ml:px" gibi...

Translated with Google Translate

Note: in the example above, values between 0-99 can be processed with approximately 20 lines of code. Even though there is no innovation regarding this, I have my own solutions. Like ="ml10ml:px"...

tabatkins commented 5 months ago

@volkantash Apologies, but the CSSWG conducts its work in English, and I don't want to trust auto translation to understand your contribution.

lazarljubenovic commented 5 months ago
  • searching for * is not easy, you have to already know the name in context (universal, wildcard) to be able to find more information.

Searching through code as plain text will always yield all sorts of things unless you get creative with regexes. It's up to IDEs to provide contextual lookup relevant to the language and help developers navigate their code.

  • reading and understanding code becomes harder when the density of ascii symbols is greater

What's the source of this? Maths seems to be doing fine for hundreds of years now with x ∈ N, x ≥ 2 and I don't see any movement that's advocating that "all natural numbers which are greater than or equal to two" is clearer.

  • people new to CSS will have a hard time learning the difference between this and the universal selector

I'm really tired of seeing this "people new to CSS will have hard time understanding things" argument. It's really not that difficult to teach someone that there's a difference between div * and .foo-*. People already understand that { starts either an object or a code block, and no modern language is trying to go back to Pascal's begin. People also understand that "bat" is either an animal or baseball equipment, or that "knight" and "night" sound the same but mean different things. All natural languages have homonyms, so this is not exclusive to English.

  • there are a lot of tools that assume that the order in a compound doesn't matter
  • there are a lot of tools that assume that the universal selector must always be the first part in a compound

There were a lot of tools that assumed that ... is invalid JavaScript and yet here we are. It's the tools' responsibility to adapt themselves as the language progresses; the growth of the language shouldn't be bound by existing tools.

romainmenke commented 5 months ago
hidden > I'm really tired of seeing this "people new to CSS are not smart enough" argument. I never said such thing. Please be careful to not attribute hostile wording to others. I said that the proposed syntax would have traits that make it harder to learn. I never made any statement about persons learning CSS.

The aspects I listed are not some veto against the proposed syntax or feature. They are only meant to be informative. It is always a balancing act between various conflicting concerns.

These aspects are also minor or short term pains, but still worth mentioning. If there is a different path that is equally good and doesn't have these aspects then maybe that should also be considered.

lazarljubenovic commented 5 months ago

I've quoted your exact words intact, I have no idea what attribution of hostility you're referring to.

Of course it's not a veto. I don't think anyone is seeing it as such, no need to worry. It's a public discussion which we're all contributing to.

astearns commented 5 months ago

@lazarljubenovic I agree with @romainmenke here. You are the one who introduced not smart enough to this discussion, and it was not accurate or necessary. Please take more care in paraphrasing other people’s contributions.

lazarljubenovic commented 5 months ago

Thanks for the feedback. I've changed it to avoid further derailing the topic. (I don't understand the difference between "not smart enough", "has hard time understanding", or "has difficulties acquiring knowledge" - it's all just different ways to express the exact same thing. Would be great if we could focus on any reason why we believe or don't believe that's the case.)

If * means "anything", then why would it be difficult to understand that a-* means "anything that starts with a-? It's using the same symbol to mean the same thing. I can't think of a more natural way to express a-* given that * on its own already has an established meaning. In fact, I'm willing to bet that most people -- especially beginners -- would expect a-* to work as per this proposal once they see what * does. If you introduce something like :has-prefix, you have to remember that. Was it :with-prefix? Was it :prefixed? Was it ::has-prefix? Was it with parenthesis after that? Now you also have :has-suffix. And you can't express natural things like .foo-*-bar unless you write :has-prefix(.foo-):has-suffix(.-bar). I'm trying to nail the use case in which not using * as a wildcard is clearer.

I understand that shorter doesn't necessarily mean easier to understand. I don't understand why this particular case is harder to understand. I understand that too many special characters in one spot can get hard to understand. I don't understand why adding a star is harder to understand. Are you aware of any studies conducted that could translate to this case? Do you have any anecdotal evidence? I've been teaching people CSS for years now and people generally don't have difficulties to grasp the syntax of selectors one bit, until :this comes into play. (And the usual "Why doesn't < parent-select?"!)

romainmenke commented 5 months ago

The difference is in the subject. Saying that something is harder to learn is only a statement about the thing itself. It isn't a statement about any individual or any group of people. I definitely didn't want to say or imply anything negative about people who are learning CSS :)


I might be looking at the proposed .foo-* syntax with the tokenizing and parsing rules too much in mind.

A wildcard selector as it exists today selects and represents an actual element that can be of any type.

Whereas * in .foo-* is more like a modifier. And it is only a modifier on the immediately preceding part.

This reminds me a bit of pseudo element selectors which should have had a proper combinator, but don't.

LeaVerou commented 5 months ago

Lots of conjecture about what is hard for beginners being thrown around 🙃

From a usability pov, it's not the density of symbols that hinders understanding, it's the recognizability. ASCII symbols in programming languages are analogous to icons in GUIs. When an icon has a widely understood meaning, it facilitates understanding, because the circuit in our brains that recognizes pictures is much faster than the neural pathway that processes written language. However, when an icon is used that has no widely understood meaning, this benefit is moot, and instead it simply adds friction to the interaction, as we have to hover over it to figure out what it does from the tooltip. If we stick to the interface, we eventually learn what the icon means, and then it goes through the faster pathway, but that can take a while and I can't remember if it ever becomes 100% as fast as it is for a widely recognized symbol. Depending on the constraints of the use case, sometimes that may be an acceptable tradeoff, but usually it isn't. A happy medium is UIs that have both, so you can fall back to the text if image recognition fails, but not every UI can afford that kind of real estate.

The same applies to programming languages: ASCII characters are their "icons". That’s why width < 500px makes more sense than max-width: 500px but Boolean(foo) is more readable than !!foo. In programming languages instead of hovering over a button to see its tooltip we try to google it, but the principle is the same: symbols that are widely understood improve understanding, and symbols that are not hinder it (at least for a while).

Often this is intuitively understood by language designers, but the specifics of what exactly is widely recognized by the target audience is the source of many a debates (e.g. for my previous example, I'm sure many would argue !!foo is perfectly fine because it's widely understood as a JS convention).

On to this particular case, I would argue using a wildcard to mean "any string of symbols" is a very widespread convention. It extends beyond programming languages as well, as it's a common convention in many searching GUIs. On the contrary, strings of symbols like ^~= are far more esoteric. Sure, you'd be able to tell that [class^~="foo"] has to do with classes, but beyond that, you'd be stuck. And once we need to come up with a selectors for element types or attribute names, we have to invent two other conventions, whereas using wildcards across the board to mean "this part is variable" means authors only need to learn a single concept that they can then apply to multiple places, which tends to be a characteristic of good UI design (few flexible concepts that can be combined in many different ways, rather than many rigid ones that have to be learned separately).

Also, whether beginners can understand the difference between .foo-* and .foo- * is a pretty easily testable hypothesis. 😄

romainmenke commented 5 months ago

Yeah, I think way too much weight is being assigned to some of the items I listed here : https://github.com/w3c/csswg-drafts/issues/10001#issuecomment-1978136727 :)

In particular people new to CSS will have a hard time learning the difference between this and the universal selector

That statement assumes that there is a perceived difference for CSS authors. If however they appear to work the same, then that entire item becomes irrelevant.

My concern here was that there would be a set of rules and behaviors for universal selectors and a different set for wildcards. If they align, then this is moot.


As I finished that comment, I am actually in favor of figuring out how to make * work. But I also don't think the feature stands or falls with this exact syntax :)


Also, whether beginners can understand the difference between .foo- and .foo- is a pretty easily testable hypothesis. 😄

Yup, easily testable :) But only relevant if there is an actual difference.

As you also say, * is a widely understood concept and for authors * might just mean "expand" the current selector to any matching element.

volkantash commented 5 months ago

Herhangi bir seçicide daha iyi seçiş yapabilmek için düşündüğüm yöntem şöyle: (The method I think of to make a better selection in any selector is as follows:)

css

div[class^="abc" + * + "z"] {
  background: #ff0000;
}

div[class$="abc" + * + "z"] {
  background: #aa0000;
}

div[class*="abc" + ** + "z"] {
  background: #bbbb00;
}

div[class*="abc" + 2* + "z"] {
  background: #ccc333;
}

div[class*="abc" + 2** + "z"] {
  background: #6789ab;
}

html

<div class="abcde"></div>
<div class="abcdexyz"></div>
<div class="abcdz"></div>
<div class="abcdez"></div>
<div class="abcdefghz"></div>

Kodları okuyarak anlayamıyorsanız ayrıntılı olarak anlatabilirim. (If you cannot understand by reading the codes, I can explain in detail.)

LeaVerou commented 4 months ago

Just remembered that even the HTML spec recommends using this pattern, to specify the language of <code> elements (and it has been a huge pain to handle in Prism).