CSS Specificity with CSS Module

5.5k Views Asked by At

First, let me say I understand that I have a custom component "Card" that I use in one of my routes, "Home".

Card.js

import s from 'Card.css';
class Card {
    ...
    render(){
        return (<div className={cx(className, s.card)}>
            {this.props.children}
        </div>);
    }
}

Card.css

.card {
    display: block;
}

Home.js

<Card className={s.testCard}>
...
</Card>

Home.css

.testCard { display: none; }

A problem I faced here, is that the card remained visible even though I set the display to none, because of seemingly random CSS ordering in the browser. This ordering did not change even if Javascript was disabled.

To get .testCard to correctly load after .card, I used "composes:":

Home.css

.testCard {
    composes: card from 'components/Card.css';
    display: none;
}

Apparently this causes css-loader to recognize that .testCard should load after .card. Except, if I disable Javascript, the page reverts back to the old behavior: the .card is loaded after .testCard and it becomes visible again.

What is the recommended way to get our page to prioritize .testCard over card that behaves consistently with or without Javascript enabled?

4

There are 4 best solutions below

2
charlietfl On

Just make the testCard rule more specific by combining classes

.card {display: block;}

.card.testCard { display: none; }
0
Essam Al-Mansouri On

As I'm using CSS modules, charlietfl solution wouldn't really work as is. .card is automatically mangled to a name like .Card-card-l2hne by the css-loader, so referencing it from a different component wouldn't work. If I import it into the CSS file of Home.css, that also doesn't work, because it creates a new class with a name like .Home-card-lnfq, instead of referring to .Card-card-l2hna.

I don't really think there's a great way to fix this so I've resorted to being more specific using a parent class instead.

As an example, Home.js:

import s from 'Home.css';
import Card from './components/Card';

class Home {
    ...
    render(){
        return (
        <div className={s.root}>
            <Card className={s.testCard}>Hi</Card>
        </div>
        );
    }
}

Home.css

.root {
    margin: 10px;
}
.root > .testCard {
    display: none;
}

This way, we don't need to know what class names component Card is using internally, especially since in cases like CSS Modules or styled components, the class name is some unique generated name.

I don't think I would have come to this solution if it wasn't for charlieftl's solution, so thank you very much for that.

0
snowyBunny On

Css specificity is calculated based on the selectors used. Elements selectors have a value of 1 Class selectors have a value of 10 ID selectors have a value of 100 Inline css have a value of 1000.

So if you specify more using these rules above you can achieve which rule is applied.

For example. in the case of making the selector

.card vs .card.testCard

.root > .testCard

(using examples posted here)

The calculated specificity values are:

  • .card = 10
  • .card.testCard = 100
  • .root > .testCard = 100

so using more specified selectors by adding either class, id or elements increase the specificity points and apply the rule with the highest specificity value.

Caveat: if the selectors that point to the same element have the same value then the latest rules win. so in the example here

<style>
.card.testCard { color: red; }
.root > .testCard { color: green; }
</style>

The style chosen here will be color of green even though both have the same specificty because latest rules win.

0
Vicente Losada Fadul On

You can use @layer from css.

Define a layer for your base UI components in your global.css

@layer components;

Then:

Card.css

@layer components {
  .card {
    display: block;
  }
}

Now your testCard class in Home.css will work as you expect. As your testCard is in an anonymous layer (it is not in any defined @layer), it will have more precedence. You can check it in this example.