[Proposal] Substring Matching of Individual Class Names

selectors
Tags: #<Tag:0x00007fa6656475b8>

#1

Note: I’ve also posted this as an issue on CSS Working Group drafts.

It’s currently possible to match substrings of attribute values, including the class attribute.

https://www.w3.org/TR/selectors-4/#attribute-substrings

It would also be useful to have the ability to match substrings of individual class names. Using the existing attribute selector syntax as a reference, this might look something like:

.classname /* A standard class selector */

.[^class] /* Matches any class name that begins with "class" */
.[$name]  /* Matches any class name that ends with "name" */
.[*ass]   /* Matches any class name that contains the substring "ass" */

Which would be compatible with the Selectors Level 4 case-sensitivity syntax:

.[^classname i] 
.[$classname i] 
.[*classname i]

Also, it could be modified to support substring matching in ID selectors:

#[^example]  #[^example i] 
#[$example]  #[$example i] 
#[*example]  #[*example i]

Such a selector would make it practical to use a naming convention whereby a .Module parent shares common styles with a .SubModule child because although this can be achieved by grouping the related classes, it becomes unwieldy as the number of children increases, for example:

.Module, .SubModule, .AnotherModule, .YetAnotherModule {…}

:matches(.Module, .SubModule, .AnotherModule, .YetAnotherModule).variant {…}

Whereas, this could be rewritten in a simpler form using a substring matching class selector, so that there is no need to manage lists of dependants and style sharing is automatic:

.[$Module] {…}         /* Equivalent to .Module */
.[$Module].variant {…} /* Equivalent to .Module.variant */

.SubModule {…}         /* Inherits styles from .Module */
.SubModule.variant {…} /* Inherits styles from .Module.variant */

.ModuleUnrelated {…} /* Not matched as name begins not ends with "Module" */

The effect is static inheritance without the use of a preprocessor e.g. @extend in Sass and Stylus.


#2

Substring matching makes a lot of sense in contexts where there can only be one value, such as the contents of input fields. However, an element can have multiple classes, which means that you can also achieve hierarchical namespacing without substring matching of classes.

E.g. instead of class="module" and class="fooModule", you can have class="module" and class="foo module". Now you can target both elements with the selector .module. And it’s more robust than substring matching, since you won’t accidentally match the wrong thing just because it contains the wrong trailing characters. And if you want to target specifically the module foo, you can still use the selector .foo.module.


#3

True. There are, however, some advantages to the single class approach over the multi-class approach.

  1. Flattened selectors eliminate any potential specificity conflicts, for example:
<div class="card product">
  <div class="card-title">…</div>
  <div class="card-body">…</div>
</div>

.card {…} /* specificity: 0,0,1,0 */
.card.product {…} /* specificity: 0,0,2,0 */
.card.product .card-title {…} /* specificity: 0,0,3,0 */

Each selector ratchets up the specificity, which doesn’t happen with single classes:

<div class="ProductCard">
  <div class="ProductCard-title">…</div>
  <div class="ProductCard-body">…</div>
</div>

.[$Card] {…} /* specificity: 0,0,1,0 */
.ProductCard {…} /* specificity: 0,0,1,0 */
.ProductCard-title {…} /* specificity: 0,0,1,0 */

There is growing acceptance that needless specificity makes CSS difficult to maintain.

  1. There is no risk of styles accidentally leaking out of a module, for example:
<div class="box one">
  <div class="box-body">
    <div class="box two">
      <div class="box-body">
        …
      </div>
    </div>
  </div>
</div>

.box.one .box-body {…} /* this will also match box two's body */

This cannot happen with single classes because each sub-module has a different namespace.

A solution might be to use a child selector but this is sub-optimal, particularly when dealing with deeply nested children, selectors become increasingly complex and specificity rises:

.box.one > .box-body {…} /* this would only match box one's body */

.box.one > .box-body > .box-something {…} /* ever more specificity! */
  1. Static inheritance is more robust than using multiple classes on an element (dynamic inheritance).

There is always a risk of a .product sub-module becoming separated from it’s .card parent, or a child element from a different sub-module being accidentally included in another sub-module:

<div class="card product">
  <div class="card-name">…</div>
  <div class="card-description">…</div>
</div>

<div class="card user">
  <div class="card-name">…</div>
  <div class="card-description">…</div>
</div>

This cannot happen with single classes without fundamentally violating the naming convention:

<div class="ProductCard">
  <div class="ProductCard-name">…</div>
  <div class="ProductCard-description">…</div>
</div>

<div class="UserCard">
  <div class="UserCard-name">…</div>
  <div class="ProductCard-description">…</div>
</div>

Don’t get me wrong, dynamic inheritance has a role to play, such as in applying context specific styles, but static inheritance without the use of a preprocessor, would be a valuable addition to CSS.

  1. It’s more readable, though this is somewhat subjective.

Unintentional matching is possible with either a single (with substring matching) or multi-class approach. It’s not a failing of substring matching per se, rather the inherit global nature of CSS.

A consistent naming convention, together with the discipline to follow it, is in my view, the only practical solution to mismatching. However, combining a substring matching selector with another selector e.g. .foo.[^bar] would also reduce the likelihood of a mismatch in certain circumstances.

Finally, this is just one use case, there are likely other uses in addition to naming conventions.


#4

I think the phrasing of this this tricky… I think it’s safe enough to say that there is a growing acceptance that specificity alone isn’t enough. However, that is why we also have proposals/standards already far along like shadow dom and a 0 specificity pseudo class. Given that there are practices to avoid this and new specs tackling related issues, I’m unsure how compelling this particular data point is to me (personally) at this time.

You can do substring matching of ids currently via attribute selectors, it will just have a lower specificity than the hash oriented one.

All this said though, and leaving aside how exactly it would be written or expressed - the platform does defines a DOMTokenList concept (an attribute containing N space separated values) and class really isn’t the only thing that uses it. A number of aria attributes, for example, can also use that idea, and who knows what else finds that primitive idea useful. It does seem like having a way to style on those universally with about the same level of power as attribute values could maybe be an interesting and useful feature…


#5

It would be a syntactic sugar. I accept that it has limited utility.