59

I am trying to use CSS Modules in my React project. First part of the problem is that if I write nested css classes (using sass), I don't know if I can access the inner ones, since those seems to be compiled into unique classnames as well. Some code:

<div className={this.state.visible ? styles.header+" menu-visible" : styles.header}>
    <div className="menu">
        <a className="link">title</a>
    </div>
</div>
.header {
    &.menu-visible {
        .menu {
            display: block;
        }
    }
}

I have a wrapping class that sometimes is "menu-visible" which changes attributes on all the children, is it bad practice to do it like this in React?

There is multiple classes inside the .header that are changed if the menu is visible, therefore it would be convinient to just change the wrapping class, can I reference the children in some way? So that the remain nested in scss?

EDIT

One solution that I can think of is to replace className="menu" with className={styles.header.menu} but that seemed not to work. The problem is that I would like .menu to change its attributes if its parent has the class menu-visible.

Stanley
  • 2,434
  • 18
  • 28
rablentain
  • 6,641
  • 13
  • 50
  • 91
  • I've ran across this discussion for next.js, maybe there will be some answers that show up there or a fix eventually: https://github.com/vercel/next.js/discussions/19417 – CTS_AE Sep 03 '21 at 20:58

6 Answers6

83

I solved it. I think that I just overdid it in my mind. Instead of writing styles.header.menu I could just write styles.menu, even if it was nested.

Example (React + JSX):

<div className={styles.header}>
  <div className={styles.menu}>
      Whatever
  </div>
</div>

.header {
   .menu {
      display: block;
   }
 }
Neurotransmitter
  • 6,289
  • 2
  • 51
  • 38
rablentain
  • 6,641
  • 13
  • 50
  • 91
  • 4
    That was actually helpful! It's not a piece of information easily found (that you can refer to __any__ nested class), so thanks a lot for bringing this up! – Neurotransmitter Jun 11 '20 at 12:01
  • 2
    One thing to note is that you do not get the strength of the nested class selection. sometimes I wish they would change this functionality or at least explain why. At this point I'm not sure if it's a bug or a feature. It's also confusing if you expect your nested class selectors to actually transpile down and exist, as they will not unless you reference them, and I believe they all become top level selectors when you go to import them. – CTS_AE Sep 03 '21 at 20:33
  • 1
    Are you able to reach a 3rd level deep nested class? – Nathan Sep 23 '21 at 17:18
  • @CTS_AE If you use the following approach you preserve the strength of the nested class selection [https://stackoverflow.com/a/70389583/4387229](https://stackoverflow.com/a/70389583/4387229) – Simon Watson Dec 17 '21 at 07:19
  • 1
    What will happen, if I will have the same name classes nested under different parents in one module? `.myModule { .parent1 { .foo {}} .parent2 { .foo {}}}` – Dinar Feb 28 '23 at 13:27
  • It looks not working if you use a dash, solution here: https://github.com/facebook/create-react-app/issues/11155 : className={styles["Event-Entries"]} – Nammen8 Mar 27 '23 at 09:30
44

An alternative solution which better preserves the nested classes and styling is to use the global scope :global on all the nested classes when using a preprocessor like sass or less.

.header {
  :global {
    .menu {
      display: none;

      &.menu-visible {
        display:block;
      }
    }
  }
}
<div className={styles.header}>
  <div className="menu menu-visible">
      Whatever
  </div>
</div>
Simon Watson
  • 1,550
  • 16
  • 16
  • 2
    awesome solution when you're working with components like bootstrap where it creates children wrappers that can't be given classNames – Facundo Colombier Nov 18 '22 at 14:29
15

You can use [class~=classname]

.header {
   [class~=menu] {
      display: block;
   }
 }

which will not be detected as a class and left alone.

sebastian.i
  • 7,911
  • 1
  • 11
  • 8
  • This can work and I've seen this brittle "hack" as a fix a few places online, but it still concerns me that a class name could be a substring of another class name at which point this doesn't work so great. ie: `[class~=cat]` would match for a class like `categories`. – CTS_AE Sep 03 '21 at 20:57
  • 1
    `[class~="cat"]` will match a word, not a substring. So, it will only match with `cat`. `[class*="cat"]` will match with `categories`. More info about this is here https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors – nleslie Jan 30 '22 at 22:29
6

Note that the solution in the accepted answer can lead to a significant increase in your css bundle size when deep nesting selectors.

Accepted solution with 1 more level of nesting

JSX:

<div className={styles.header}>
  <ul className={styles.menu}>
    <li className={styles.item}>
      Whatever
    </li>
  </ul>
</div>

SCSS:

.header {
   .menu {
      .item {
        display: block;
      }
   }
 }

Output (assuming default settings for css modules):

.myComponent_header__27ep6 .myComponent_menu__32Qvy  .myComponent_item__2RWLN {
  display: block
}

Alternative solution

A better way of writing it would be to borrow loosely to the BEM methodology and use the parent selector :

JSX:

<div className={styles.header}>
  <ul className={styles.headerMenu}>
    <li className={styles.headerMenuItem}>
      Whatever
    </li>
  </ul>
</div>

SCSS:

.header {
   &Menu {
     &Item {
       display: block;
     }
   }
 }

Output:

.myComponent_headerMenuItem__37djq { display: block }
drskullster
  • 781
  • 7
  • 20
0

I don't know if it will help but i found the way as follows:

file: package.json

"dependencies": {
  ...,
  "sass": "^1.49.9"
}

file: test.scss

.box {
    width: 100%;
}

.boxItem {
    background: red;

    &.first {
        background: blue;
    }

    &.second {
        background: silver;
    }
}

file test.js

import React from 'react'
import styles from "./test.scss";

const test = () => {
  return (
    <div className={`${styles.box}`}>
      <div className={`${styles.boxItem} ${styles.first}`}>Test 01</div>
      <div className={`${styles.boxItem} ${styles.second}`}>Test 02</div>
      <div className={`${styles.boxItem}`}>Test 03</div>
    </div>
  );
};

export default test;

0

Not quite sure why the horrendous hack above is the accepted answer. Maybe it's worked for someone in some odd context.

  1. the code posted doesn't work
  2. this fundamentally breaks the 'C' in CSS! Now it could be successfully argued that css modules are themselves breaking the 'cascading' nature of CSS, but equally, its goal is to limit the cascade to a certain scope...which as we can see from the question and its various 'solutions', it's been only partially successful. It is such a bummer it has to be this way!

A more 'CSS-friendly' way to solve this is to explicitly state the class hierarchy in the subclass name, like this (in e.g. mymenu.module.css):

.header {
}

.headerMenu {
  display: block;
}

Note that in this case you can't use the bracket notation, as CSS modules won't see it - so the class name can't have hyphens and it can't be 'normally' cascaded ¯\(ツ)/¯ - e.g.:

.header-menu {
  display: block;
}

and/or

.header .menu {
  display: block;
}

will not work, as the bracket selector notation (yet another CSS hack!) simply won't recognize it.

While this is still a [local] CSS hack, at least it's restricted to the classes at hand, doesn't bloat the size of your bundle, and keeps your CSS as well, CSS.

Midiman
  • 174
  • 4
  • 7