csjones1
csjones1

Reputation: 53

Next.js - Cant apply dynamic class names when using CSS Modules

hoping someone here can help me solve this.

Am trying to build a website through NextJs. One of my pages has some paragraphs and buttons which are styled differently based on states and events. I can get the styling to work as intended when using pure React, and also when using a Global Stylesheet with NextJs; but when I use CSS Modules I cant get it to function as intended.

(Note: I can also get it to work by using a simple ternary like <h1 className={submitted ? styles.showresult : styles.hideresult}>Correct? {correct}</h1>; but I have some other scenarios where I need to rely on an multiple ifs and create multiple classes, each with their own styling, so I cant make a simple ternary my final solution.

E.g. this is the file pagex.js

import React from 'react';
import ReactDOM from 'react-dom';

const Pagex = () => {

const [submitted, setSubmitted] = React.useState(false); // whether the submit button was pressed

function calculateScore() {
  let correct = 0
  let incorrect = 0
    //......some scoring logic.....    
  setSubmitted(true)
  }

  // function to create a display class based on whether the submit button has been pressed
 function displayResult(){
   if (submitted === true) {
      return "showtheresult"
    } else {
         return "hidetheresult"
    }
  }

return (
  <section className="results">
      <h1 className={displayResult()}>Correct? {correct}</h1>
      <h1 className={displayResult()}>Incorrect? {incorrect}</h1>
  <button className={displayResult()} onClick={handleMovClick}>An instruction</button>
    </section>
    </div>
  );
};
export default Pagex;

the globals.css file contains

h1.hidetheresult, h3.hidetheresult {
  visibility: hidden;
}

h1.showtheresult, h3.showtheresult {
  visibility: visible;
}

button.hidetheresult {
    border-color: pink;
  }

button.showtheresult {
    border-color: aqua;
  }

Changes when using CSS modules

  1. Add a CSS file in the correct folder with the correct name (../styles/Pagex.module.css) which contains the same styling shown above
  2. Additional import in pagex.js import styles from '../styles/Pagex.module.css'
  3. Change reference in the function within pagex.js
    function displayResult(){
       if (submitted === true) {
          return {styles.showtheresult}
        } else {
             return {styles.hidetheresult}
        }
      }

When i do this the '.' in {styles.showtheresult} and {styles.hidetheresult} gets highlighted as an error by vscode with this detail: ',' expected. ts(1005). Saving the js with a dev server running shows a similar message after trying to compile: Syntax error: Unexpected token, expected "," and the browser shows the same message along with "Failed to compile"

Also tried just passing styles.showtheresult / styles.hidetheresult by removing the curly braces from the displayResult() function. That compiles but nothing happens on the compiled webpage, i.e the class doesnt get updated when the button is pressed and so the styling cant be applied.

Also Tried passing as ${styles.showresult} and ${styles.hideresult} (with `)in the return statement. That also compiles but the page itself gives me an "Unhandled Runtime Error ReferenceError: styles is not defined" message and I cant load the page.

Would highly appreciated if someone could help correct my syntax in the function itself or elsewhere in the code.

Upvotes: 5

Views: 20464

Answers (1)

PsyGik
PsyGik

Reputation: 3675

Because you asked nicely ;) (just kiddin')

So Next.js is an opinionated framework and uses CSS Modules to enforce component scoped styling.

Basically you define your stylesheet with a name.module.css filename and add regular CSS in it.

.hidetheresult {
    visibility: hidden;
}

.showtheresult{
    visibility: visible;
}
  
.btn-hidetheresult {
    border-color: pink;
}

.btn-showtheresult {
    border-color: aqua;
}

Now to use this, import it like any JS module,

import styles from './styles.module.css'
console.log(styles);
// styles => {
//  hidetheresult: 'contact_hidetheresult__3LvIF',
//  showtheresult: 'contact_showtheresult__N5XLE',
//  'btn-hidetheresult': 'contact_btn-hidetheresult__3CQHv',
//  'btn-showtheresult': 'contact_btn-showtheresult__1rM1E'
//  }

as you can see, the styles are converted to objects and now you can use them like styles.hidetheresult or styles['btn-hidetheresult'].

Notice the absence of element selector in the stylesheet. That's because CSS Modules rewrite class names, but they don't touch tag names. And in Next.js that is the default behaviour. i.e it does not allow element tag selectors.

File extensions with *.module.(css | scss | sass) are css modules and they can only target elements using classnames or ids and not using tag names. Although this is possible in other frameworks like create-react-app, it is not possible in next-js.

But you can override it in the next.config.js file. (Beyond the scope of this answer)

There is an article which explains how to override it. - disclaimer: I am the author


Now coming to your use-case, you can do contitional styling like so: (assuming the styles are as per the sample given in the answer)

import React from "react";
import styles from "./styles.module.css";

const PageX = () => {
  const [submitted, setSubmitted] = React.useState(false);

  const getStyle = () => {
    if (submitted) return styles.showtheresult;
    else return styles.hidetheresult;
  };

  const getButtonStyle = () => {
    if (submitted) return styles["btn-showtheresult"];
    else return styles["btn-hidetheresult"];
  };

  return (
    <div>
      <section className="results">
        <h1 className={getStyle()}>Correct?</h1>
        <h1 className={getStyle()}>Incorrect?</h1>
        <button className={getButtonStyle()} onClick={handleMovClick}>
          An instruction
        </button>
      </section>
    </div>
  );
};

As you add more conditions, the methods do tend to get more complex. This is where the classnames module comes handy.

import styles from "./styles.module.css";
import clsx from "classnames";

const PageX = () => {
  const [submitted, setSubmitted] = React.useState(false);

  const headerStyle = clsx({
    [styles.showtheresult]: submitted,
    [styles.hidetheresult]: !submitted,
  });
  const btnStyle = clsx({
    [styles["btn-showtheresult"]]: submitted,
    [styles["btn-hidetheresult"]]: !submitted,
  });
  return (
    <div>
      <section className="results">
        <h1 className={headerStyle}>Correct?</h1>
        <h1 className={headerStyle}>Incorrect?</h1>
        <button className={btnStyle} onClick={handleMovClick}>
          An instruction
        </button>
      </section>
    </div>
  );
};

Here's a CodeSandbox for you to play with: Edit 68554803-next-js-cant-apply-dynamic-class-names

Upvotes: 13

Related Questions