Dev & EngARTICLE

The class prefix selector arrives in the CSS Selectors Level 5 draft

The .btn-* syntax promises to replace verbose substring selectors, but still runs into support limitations and specific writing rules.

The class prefix selector arrives in the CSS Selectors Level 5 draft
Image: Carina Ferreira

Anyone who works with design systems knows the pain: a family of classes sharing the same prefix (.btn-primary, .btn-secondary, .btn-danger) and the need to apply base styles to all of them without repeating the declaration block. The class prefix selector proposal, covered by Geoff Graham on CSS-Tricks based on a post by Bramus, tackles exactly this case with a lean syntax: .btn-*.

What's new isn't the idea itself (Lea Verou proposed it back in 2024), but the fact that it was formally adopted and added to the Selectors Level 5 draft just days before publication. That shifts the status from "community wish" to "official path in the specification," which usually precedes actual implementations in browsers.

What exists today and why it's a pain

To select all classes sharing a common prefix, developers basically have three current options today, all of them with problems. The first is to list everything by hand:

css
.btn-primary,
.btn-secondary,
.btn-danger {
  padding: 0.5rem 1rem;
  border-radius: 4px;
}

It works, but it doesn't scale: every new variant requires editing the selector. The second is to resort to substring attribute selectors, which catch any class starting with btn- or containing btn- (the space covers the case where the class isn't the first one in the attribute):

css
[class^="btn-"],
[class*=" btn-"] {
  padding: 0.5rem 1rem;
}

This pattern gets the job done, but it's verbose, hard to read, and, according to Bramus's argument, has a performance cost. The third alternative would be to migrate to data attributes ([data-variant]), which forces you to touch the HTML and still keeps the verbosity in the CSS.

How it looks with the prefix selector

The proposal condenses all of this into a single line that reads almost like a regular class:

css
.btn-* {
  padding: 0.5rem 1rem;
  border-radius: 4px;
}

Ergonomics is the main draw. There's no manual listing, no attribute syntax, and the selector makes it explicit that it's dealing with a family of variants. Graham compares the gain to what happened with color functions, which gained shorter forms over time, though it's worth noting that the piece itself contains a syntax error in the color examples, corrected in the comments by Ana Tudor: in rgb() the new notation uses spaces between components, and numeric values without % operate in the 0 to 255 range, not as a percentage.

The rules that limit the wildcard

The * here isn't a free wildcard. It only matches the pattern of a prefix followed by a hyphen, and some constructions are explicitly left out:

css
.prefix* {}          /* not valid */
.prefix-*-suffix {}  /* not valid */
.prefix_* {}         /* the door was left open for underscore */

The mandatory hyphen created some discomfort among developers. In the post's comments, Wim notes that supporting only the hyphenated format "is kind of weird," even though it makes sense as a direct substitute for [class^=] when the prefix needs to be the first class among several.

Another point relevant to anyone dealing with the cascade: the draft implies, but does not explicitly state, that .prefix- has the same specificity as a class selector, that is, (0,1,0). That's the expected behavior, since .btn- shouldn't be stronger than writing .btn-primary directly. Confirming this in the final specification matters: a different specificity would silently change which rule wins in conflicts.

There's also room for nested use, which would pair well with the nesting syntax already available:

css
.prefix {
  &-* { /* ... */ }
}

And Dave Rupert left the obvious request in the comments: extend the same mechanism to select custom elements, something that today also depends on attribute selectors or manual listing.

The honest question: does this solve something new?

Graham himself hesitates, and that hesitation is the most useful part of the piece. Brian Kardell, from Igalia, raised the question that remains open: if the argument in favor is performance, why couldn't the existing [class^="btn-"] be optimized the same way? In his words, "it seems like, if we can optimize this, we could optimize that." In other words, the new selector may be working around an implementation problem rather than justifying a genuinely new feature. Without details on how browsers intend to optimize .prefix-*, the performance gain remains a promise.

What changes in practice (and why it still doesn't)

The detail that most affects day-to-day work is that this isn't a natural progressive enhancement. Unlike the short color notation, which degrades predictably, a .prefix-* selector simply does nothing in browsers that don't understand it. In practice, anyone who wants to adopt it early will have to wrap the rules in a support check:

css
@supports selector(.prefix-*) {
  /* styles that depend on the prefix */
}

And there's the paradox pointed out in the article: if the big appeal is ergonomics, wrapping everything in @supports and keeping a fallback with the old selectors until the feature becomes Baseline cancels out much of the code savings. As long as support isn't widespread, developers keep both versions and end up writing more, not less.

For the Brazilian team, the pragmatic takeaway is straightforward: it's worth following the progress in Selectors Level 5 and Bramus's channels, but there's no reason to rewrite codebases now. Substring selectors remain valid, have other use cases, and don't become obsolete. The moment to adopt .prefix-* without a fallback is when it reaches Baseline status, and that timeline, as Graham himself admits, is still unknown.

Translated from the Brazilian Portuguese original · Read the original

Read also