DEV Community

Daniel Pertu
Daniel Pertu

Posted on

A CSS triangle vanished, because one selector matched by substring

Our stylesheet contains this:

[class*='bg-card'] {
  border: none !important;
  border-width: 0 !important;
}
Enter fullscreen mode Exit fullscreen mode

Cards are borderless by design, and this one rule replaced four overlapping copies that all did the same thing in different places. It is deliberate, it is documented, and the substring match is genuinely necessary: Tailwind writes opacity modifiers into the class name itself, so bg-card/90 has to be caught too, and no other utility in the framework contains the string bg-card.

It is also a rule that quietly eats an entire category of change, and the comment above it says so:

Consequence worth knowing: a border utility on a Card is silently dead. Use a tinted surface for emphasis instead.

Nothing warns you. You add border border-border to a <Card>, it typechecks, the class lands in the DOM, devtools shows the declaration struck through, and if you did not think to open devtools you conclude that your border colour is just very subtle.

Its neighbour was the same idea applied one step too far, and that one did real damage.

The rule that deleted a triangle

The sibling rule suppresses the divider line on sections that opt into one. It used to be written like this:

[class*='border-t'] {
  border-top: none !important;
}
Enter fullscreen mode Exit fullscreen mode

Read what that matches. Not border-t. Every class whose name contains border-t:

  • border-t (intended)
  • border-t-2, border-t-4 (probably fine)
  • border-teal-500 (not a top border at all, it is a colour)
  • border-transparent (also a colour)
  • border-t-foreground (a top border someone chose deliberately)
  • border-t-24 (a width, and in our case a shape)

Two things broke, and they broke in a way that no test could catch.

Teal bordered circles in the UI lost their top edge. That one is merely wrong looking, and someone eventually noticed.

The second is my favourite bug of the year. One of the assessments is a balance puzzle, and its fulcrum is a pure CSS triangle. The classic trick:

<div class="border-t-24 border-x-12 border-x-transparent border-t-slate-400"></div>
Enter fullscreen mode Exit fullscreen mode

A CSS triangle is its borders. There is no element to see once they are gone. Removing the top border does not shrink the triangle or change its colour, it deletes the entire shape, and the balance puzzle rendered with its beam floating over nothing.

The fix is one character of specificity:

.border-t {
  border-top: none !important;
  border-top-width: 0 !important;
}
Enter fullscreen mode Exit fullscreen mode

An exact class selector. border-teal-500 and border-t-24 are no longer matched, because they are different classes that merely share a prefix with the one we meant.

Why substring selectors are so tempting

[class*='...'] looks like the obvious tool when you want to catch a family of utilities, and in a utility CSS framework the family really is expressed in the class name. The trap is that utility class names are a namespace with no delimiters and no guarantee of uniqueness. border-t, border-teal-500 and border-transparent are three unrelated utilities that share six characters, and the selector cannot tell them apart because there is nothing in the string to tell apart.

The test that makes it safe is specific, and it is worth running mentally every time you reach for the attribute selector:

Can any other class in the framework contain this string? For bg-card, no, and the opacity modifier means you need the substring. For border-t, obviously yes, several. The first is a legitimate use, the second is a landmine, and the difference is not a matter of style.

The wider problem, and the rules we ended up with

Global !important overrides are a layer that silently removes capabilities from every component below them. They are invisible at the call site, which means every engineer who joins rediscovers them one bug at a time. Our provider expansion had several people working in parallel, and two of them lost an afternoon each to the bg-card rule before it went into the shared onboarding doc as a known hazard:

Around line 449 it carries [class*='bg-card'] { border: none !important }. The selectors match by substring, so any class containing those fragments is hit and every option button styled from a bg-card variant renders as bare text with no visible target. It typechecks and tests green and is visible only on screen.

Three rules came out of it.

Document the consequence, not just the intent. "Cards are borderless by design" tells you what the rule wants. "A border utility on a Card is silently dead, use a tinted surface instead" tells you what to do when you hit it. The second is the sentence that saves the afternoon, and it belongs in the stylesheet next to the rule, not in a wiki.

Prefer an exact selector, and pay the cost of listing variants. If you need to catch three specific classes, catch three specific classes. An enumerated list that someone has to extend is better than a pattern that catches things nobody has written yet.

Verify on screen, not in the class attribute. This is the part that generalises past CSS. Every one of these failures typechecks, passes tests, and puts the correct class in the DOM. The only instrument that detects them is a human looking at the pixels, at the size they will actually be viewed at. Our build guidance says it outright: app/globals.css contains broad overrides that can silently defeat utility classes, so verify the result at phone width rather than reasoning about class names.

Go and grep

The surviving rule is visible on any public page. Open the pricing page or the comparison page, inspect one of the cards, and look for a border declaration with a line through it. That is the bg-card rule doing its job, and it is the same mechanism that deleted the triangle when it was aimed one utility further along.

The triangle itself belongs to the Balance puzzle, listed on the Arctic Shores hub. Its fulcrum is three borders on an empty div, and for a while it was not there at all.

Then run this against your own stylesheet:

grep -n "class\*=" app/globals.css
Enter fullscreen mode Exit fullscreen mode

For each hit, ask whether any other class in your framework contains that substring. If you find one that does, you have almost certainly already shipped a component that quietly does not work, and nobody has filed it, because a missing border does not look like a bug. It looks like a design decision.

Top comments (1)

Collapse
 
florian131313 profile image
G

border-t matching border-teal and border-transparent is easy to miss. DevTools shows the declaration crossed out; on the page it just looks like someone wanted no top edge. I use [class*='...'] for Tailwind opacity suffixes the same way they did on bg-card. For border-t several other utilities contain that fragment, so an exact class would have left the triangle alone.