← Back

React.js + Next.js · Module 2. React Basics · Lesson 11

Class Composition with clsx

Pizza Palace components often need different styles depending on their props or current state:

  • a successful order message should be green;
  • a payment error should be red;
  • a primary order button should stand out;
  • a disabled button should look and behave inactive;
  • an available pizza should show a green status;
  • a sold-out pizza should show a red status.

Instead of building complicated className strings manually, we can use the small clsx library. clsx combines class names and includes them conditionally.

You will learn

  1. What class composition means.
  2. How to install and import clsx.
  3. How clsx() combines class names.
  4. How to include classes conditionally.
  5. How to combine clsx with CSS Modules.
  6. How to select dynamic classes using props.
  7. How to type style variants with TypeScript.
  8. How to represent disabled states correctly.
  9. Common clsx mistakes.

REACT · 01

1. What Is Class Composition?

Class composition means assigning several CSS classes to one element.

.button {
  padding: 10px 20px;
  border: 0;
  border-radius: 8px;
}

.primary {
  background-color: #d92d20;
  color: white;
}
function OrderButton() {
  return (
    <button className="button primary">
      Add to cart
    </button>
  );
}

The base class controls shared button styling, while the variant class controls its colour.

Browser Result

The element receives both the base and primary classes.

When classes must change at runtime, clsx makes this composition easier to read.


REACT · 02

2. What Is clsx?

clsx is a small JavaScript utility that builds class-name strings. Install it inside the React project:

npm install clsx

Import its default export:

import clsx from "clsx";

Then combine class names:

const className = clsx("button", "primary");

console.log(className);

Console Result

button primary

clsx only creates a string. It does not install visual styles or generate CSS rules.


REACT · 03

3. Using clsx in JSX

Call clsx() inside the JSX expression assigned to className.

.button {
  padding: 10px 20px;
  border: 0;
  border-radius: 8px;
}

.primary {
  background-color: #d92d20;
  color: white;
}
import clsx from "clsx";

export default function OrderButton() {
  return (
    <button className={clsx("button", "primary")}>
      Order Margherita
    </button>
  );
}

The browser receives markup equivalent to <button class="button primary">.

Browser Result

clsx() supplies the two existing CSS class names.

REACT · 04

4. Conditional Classes with clsx

Use a logical AND expression when a class should appear only under one condition.

import clsx from "clsx";

const isSelected = true;

const className = clsx(
  "pizzaCard",
  isSelected && "selected"
);

console.log(className);

Console Result

pizzaCard selected

If isSelected is false, the expression returns false, which clsx ignores:

pizzaCard

The base class is always included; the state class is conditional. clsx also ignores null, undefined, and empty strings.


REACT · 05

5. Multiple Conditional Classes

Several independent conditions can be composed in one call.

import clsx from "clsx";

const isSelected = true;
const isSoldOut = false;
const hasDiscount = true;

const className = clsx(
  "pizzaCard",
  isSelected && "selected",
  isSoldOut && "soldOut",
  hasDiscount && "discounted"
);

console.log(className);

Console Result

pizzaCard selected discounted

Each condition is evaluated separately. This is clearer than a long chain of manual string concatenations.


REACT · 06

6. Combining clsx with CSS Modules

With CSS Modules, clsx combines the generated class-name strings stored in the imported object.

OrderAlert.module.css

.alert {
  padding: 12px 16px;
  border-radius: 8px;
  color: white;
  background-color: #475467;
}

.success {
  background-color: #16803c;
}

.error {
  background-color: #b42318;
}

OrderAlert.tsx

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

export default function OrderAlert() {
  return (
    <p className={clsx(styles.alert, styles.success)}>
      Your pizza order was accepted!
    </p>
  );
}

Browser Result

Your pizza order was accepted!

The base alert class supplies spacing; the success class supplies the green background.

REACT · 07

7. Dynamic Classes Using Props

One alert component can support default, success, and error appearances.

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

interface OrderAlertProps {
  type?: "success" | "error";
  children: string;
}

export default function OrderAlert(props: OrderAlertProps) {
  return (
    <p
      className={clsx(
        styles.alert,
        props.type && styles[props.type]
      )}
    >
      {props.children}
    </p>
  );
}

The union type permits only "success", "error", or an omitted value. styles.alert is always included. The variant class is included only when type exists.

This is a good use of props: the parent chooses the meaning, and the component translates that meaning into presentation.


REACT · 08

8. Understanding styles[props.type]

Suppose Vite provides this simplified mapping:

const styles = {
  alert: "_alert_a1b2c_1",
  success: "_success_a1b2c_2",
  error: "_error_a1b2c_3"
};

Dot notation uses a fixed property:

styles.success

Bracket notation can use a dynamic property name:

const type = "success";
styles[type];

When props.type is "error", styles[props.type] is equivalent to styles.error.

The check is important when the prop is optional:

props.type && styles[props.type]

Without a value, the expression becomes undefined, and clsx safely leaves the variant class out.


REACT · 09

9. Using the Alert Component in App.tsx

Use the same typed component for three order states.

import OrderAlert from "./components/OrderAlert";

export default function App() {
  return (
    <main>
      <h1>Order Status</h1>
      <OrderAlert>Check your delivery details.</OrderAlert>
      <OrderAlert type="success">
        Your pizza order was accepted!
      </OrderAlert>
      <OrderAlert type="error">
        Payment failed. Please try again.
      </OrderAlert>
    </main>
  );
}

Browser Result

Order Status

Check your delivery details.

Your pizza order was accepted!

Payment failed. Please try again.

Props select the variant while the base alert design remains consistent.

REACT · 10

10. Why Use clsx Instead of Manual Strings?

Simple concatenation can work:

const className = styles.alert + " " + styles.success;

But conditions quickly become difficult to scan:

const className =
  styles.card +
  (isSelected ? " " + styles.selected : "") +
  (isSoldOut ? " " + styles.soldOut : "");

With clsx, each rule is visible on its own line:

const className = clsx(
  styles.card,
  isSelected && styles.selected,
  isSoldOut && styles.soldOut
);

clsx handles spacing and ignores false values.


REACT · 11

11. Using an Object with clsx

clsx also accepts objects. Each key is a class name and its value decides whether the class is included.

import clsx from "clsx";

const isSelected = true;
const isSoldOut = false;

const className = clsx({
  pizzaCard: true,
  selected: isSelected,
  soldOut: isSoldOut
});

console.log(className);

Console Result

pizzaCard selected

With CSS Modules, use computed property names:

const className = clsx({
  [styles.card]: true,
  [styles.selected]: isSelected,
  [styles.soldOut]: isSoldOut
});

Both the argument-list form and object form are valid. Choose the one that is clearest for the component.


REACT · 12

12. Practical Example — Reusable Button Variants

Create one button component for primary, secondary, and disabled actions.

OrderButton.module.css

.button {
  padding: 10px 20px;
  border: 0;
  border-radius: 8px;
  cursor: pointer;
  font: inherit;
  font-weight: 700;
}

.primary {
  background-color: #d92d20;
  color: white;
}

.secondary {
  background-color: #e4e7ec;
  color: #1d2939;
}

.disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

OrderButton.tsx

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

interface OrderButtonProps {
  text: string;
  variant?: "primary" | "secondary";
  disabled?: boolean;
}

export default function OrderButton(props: OrderButtonProps) {
  return (
    <button
      type="button"
      disabled={props.disabled}
      className={clsx(
        styles.button,
        props.variant && styles[props.variant],
        props.disabled && styles.disabled
      )}
    >
      {props.text}
    </button>
  );
}

App.tsx

import OrderButton from "./components/OrderButton";

export default function App() {
  return (
    <main>
      <OrderButton text="Add to cart" variant="primary" />
      <OrderButton text="View ingredients" variant="secondary" />
      <OrderButton
        text="Sold out"
        variant="primary"
        disabled={true}
      />
    </main>
  );
}

Browser Result

The variant prop changes colour; the disabled prop adds both styling and the real HTML attribute.

A disabled-looking class alone does not prevent interaction. The disabled attribute supplies the correct browser behaviour.


REACT · 13

13. Practical Application — Pizza Availability

Pizza availability changes both text and styling.

PizzaCard.module.css

.card {
  padding: 20px;
  border: 1px solid #e4e7ec;
  border-radius: 12px;
}

.status {
  display: inline-block;
  padding: 6px 10px;
  border-radius: 999px;
  font-weight: 700;
}

.available {
  color: #116329;
  background-color: #dcfce7;
}

.unavailable {
  color: #991b1b;
  background-color: #fee2e2;
}

PizzaCard.tsx

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

interface PizzaCardProps {
  name: string;
  price: number;
  available: boolean;
}

export default function PizzaCard(props: PizzaCardProps) {
  return (
    <article className={styles.card}>
      <h2>{props.name}</h2>
      <p>{props.price.toFixed(2)} €</p>
      <p
        className={clsx(
          styles.status,
          props.available
            ? styles.available
            : styles.unavailable
        )}
      >
        {props.available ? "Available" : "Sold out"}
      </p>
    </article>
  );
}

App.tsx

import PizzaCard from "./components/PizzaCard";

export default function App() {
  return (
    <main>
      <h1>Pizza Palace Menu</h1>
      <PizzaCard name="Margherita" price={8.5} available={true} />
      <PizzaCard name="Truffle Pizza" price={14} available={false} />
    </main>
  );
}

Browser Result

Pizza Palace Menu

Margherita

8.50 €

Available

Truffle Pizza

14.00 €

Sold out
The base status class is always present; one availability class is selected by the Boolean prop.

REACT · 14

14. Common Beginner Mistakes

Mistake 1 — Forgetting to Install or Import clsx

Run npm install clsx, then add import clsx from "clsx" in every component that calls it.

Mistake 2 — Thinking clsx Creates CSS

clsx("button", "primary") returns a string. The .button and .primary rules must already exist.

Mistake 3 — Forgetting the Base Class

Incorrect:

className={clsx(props.type && styles[props.type])}

Correct:

className={clsx(
  styles.alert,
  props.type && styles[props.type]
)}

Mistake 4 — Using styles.type

styles.type looks for a class literally named type. Use styles[props.type] to read the class named by the prop.

Mistake 5 — Accessing an Optional Key Without a Check

When type can be undefined, guard the dynamic access with props.type && styles[props.type].

Mistake 6 — Referencing a Missing Module Class

If .warning does not exist, styles.warning is undefined. clsx ignores it, so the expected design silently disappears.

Mistake 7 — Using Only a Disabled Class

A CSS class changes appearance but not behaviour. Also pass disabled={true} to a native button.

Mistake 8 — Passing Boolean Values as Class Names

Do not write className={isSelected}. className needs a string; use className={clsx(isSelected && styles.selected)}.


REACT · 15

15. Practice Exercises

Exercise 1 — Combine Two Classes

What does clsx("button", "primary") return? Expected result: button primary.

Exercise 2 — Conditional Selection

Create const isSelected = true and include "selected" only when it is true.

Exercise 3 — CSS Module Card

Create a module with .card and .featured. Apply .card always and .featured conditionally.

Exercise 4 — Alert Variants

Create a typed OrderAlert whose optional type is "success" | "error". Render all three appearances.

Exercise 5 — Object Syntax

Rewrite a clsx(styles.card, isSoldOut && styles.soldOut) call using the object form and computed keys.

Exercise 6 — Order Button

Create primary and secondary variants plus a real disabled state. Confirm that the disabled button cannot be activated.

Exercise 7 — Pizza Availability

Use one PizzaCard component to render an available pizza and a sold-out pizza with different status classes.


REACT · 16

16. Key Takeaways

  • Class composition combines multiple CSS classes on one element.
  • clsx() returns a space-separated class-name string.
  • clsx does not create CSS rules.
  • False, null, undefined, and empty-string values are ignored.
  • A base class can be combined with any number of conditional classes.
  • CSS Module values such as styles.card are generated class-name strings.
  • Bracket notation selects a module class dynamically.
  • TypeScript unions restrict component variants to supported values.
  • The clsx object form maps class names to Boolean conditions.
  • Visual disabled styling should accompany the native disabled attribute.
  • Props describe the component state; clsx translates that state into a class list.

REACT · 17

17. Further Reading

Next lesson: Begin Module 3 with React forms, FormData, callback props, and Pizza Palace order submission.

← Back↑ Back to the top