How to Add a Class to a React Component (className, State and clsx)

To add a class to a React component, pass a string to the className prop, as in <div className="plan-card">. For classes that change, build that string from props or state, and use clsx once the conditions pile up.

<div className="plan-card">Team</div>

<button className={clsx("plan-card", { "is-selected": selected })}>

My test bench for this one: React 19.3, TypeScript 7.0.2, clsx 2.1.1 and Vite 8.3 on Windows 11, with every page rendered in Microsoft Edge so the class names you’ll see in the console are real.

In this tutorial, I’ll build a small pricing-plan picker and use it to show you how to add a class to a React component in five steps, from one static class up to classes that react to clicks.

Here’s what we’re building:

A React plan picker with four pricing cards where the Team card has the is-popular and is-selected classes and the Legacy card is dimmed
The finished picker: Team carries two extra classes, Legacy carries is-retired.

Before You Start: A React Plan Picker Sample

You need Node.js and a React + TypeScript project. If you don’t have one, a Vite project takes a minute, and it pairs well with typed function components:

npm create vite@latest plan-picker -- --template react-ts
cd plan-picker
npm install
npm install clsx
npm run dev

Here’s the sample data every step reuses. It’s four plans, in US dollars per month:

PlanPriceFlagsExtra class it earns
Starter$9.00nonenone
Team$29.00popular: trueis-popular
Business$79.00nonenone
Legacy$19.00retired: trueis-retired

Put the styles in src/App.css. These are the class rules (the full file adds a few layout lines):

.plan-card {
  width: 170px; padding: 14px 16px; text-align: left; font: inherit;
  background: #fff; border: 1px solid #c8ced6; border-radius: 10px;
  cursor: pointer; color: #1f2933;
}
.plan-card .name { display: block; margin-bottom: 6px; font-weight: 600; }
.plan-card .price { display: block; font-size: 22px; font-weight: 700; }
.is-popular { border: 2px solid #f59f00; }
.is-popular::after {
  content: "Most popular"; display: block; margin-top: 6px;
  font-size: 12px; color: #b35c00;
}
.is-retired { opacity: 0.45; cursor: not-allowed; }
.is-selected {
  background: #e7f5ff; border-color: #1c7ed6; box-shadow: 0 0 0 3px #a5d8ff;
}
.compact { width: 130px; padding: 8px 10px; }

Step 1: Add a Static className to the Component

JSX uses className, not class, because class is a reserved word in JavaScript. If the component idea itself is new to you, start with what a React component is.

Replace src/App.tsx with this. The CSS file is imported once at the top, and every card gets the same plan-card class:

import "./App.css";

type Plan = { name: string; price: number };

const plans: Plan[] = [
  { name: "Starter", price: 9 },
  { name: "Team", price: 29 },
  { name: "Business", price: 79 },
];

function PlanCard({ plan }: { plan: Plan }) {
  return (
    <div className="plan-card">
      <span className="name">{plan.name}</span>
      <span className="price">${plan.price.toFixed(2)}/mo</span>
    </div>
  );
}

export default function App() {
  return (
    <div className="plans">
      {plans.map((p) => <PlanCard key={p.name} plan={p} />)}
    </div>
  );
}
Three React pricing cards for Starter, Team and Business, each styled by the static plan-card class
One static class styles all three cards.

Checkpoint: you should see three white cards with rounded borders. If they’re unstyled, the import "./App.css" line is missing.

Step 2: Combine Multiple CSS Classes in className

Now Team should stand out and Legacy should look retired. The first idea most of us try is a template literal with a ternary and &&, the same tools you’d use for conditional rendering in React:

// inside PlanCard
const classes =
  `plan-card ${plan.popular ? "is-popular" : ""} ${plan.retired && "is-retired"}`;

return <div className={classes}>{/* same spans as Step 1 */}</div>;

What the browser got (one line per card):

"plan-card  undefined"
"plan-card is-popular undefined"
"plan-card  undefined"
"plan-card  is-retired"

That’s the trap. A ternary leaves an empty string behind, so you get double spaces. And && returns the left side when it’s falsy, which puts undefined in the class list as a word.

Put the parts in an array, drop the falsy ones with filter(Boolean), and join with one space:

// inside PlanCard
const classes = [
  "plan-card",
  plan.popular && "is-popular",
  plan.retired && "is-retired",
].filter(Boolean).join(" ");

Class names after the fix:

"plan-card"
"plan-card is-popular"
"plan-card"
"plan-card is-retired"
Four React pricing cards where Team has an orange is-popular border and Legacy is faded by the is-retired class
Team gets is-popular and Legacy gets is-retired, with no stray words.

Step 3: Toggle a Class on Click with useState

A class that follows user input comes from state. Keep the picked plan name in useState, compare it in each card, and add is-selected when it matches. The click wiring is plain React event handling.

I switched the card to a <button> here, so it’s keyboard-friendly and disabled works for Legacy:

type CardProps = {
  plan: Plan;
  selected: boolean;
  onSelect: () => void;
};

function PlanCard({ plan, selected, onSelect }: CardProps) {
  const classes = [
    "plan-card",
    plan.popular && "is-popular",
    plan.retired && "is-retired",
    selected && "is-selected",
  ].filter(Boolean).join(" ");

  return (
    <button
      type="button"
      id={`plan-${plan.name}`}
      className={classes}
      disabled={plan.retired}
      onClick={onSelect}
    >
      <span className="name">{plan.name}</span>
      <span className="price">${plan.price.toFixed(2)}/mo</span>
    </button>
  );
}

export default function App() {
  const [picked, setPicked] = useState("Starter");

  return (
    <>
      <div className="plans">
        {plans.map((p) => (
          <PlanCard
            key={p.name}
            plan={p}
            selected={picked === p.name}
            onSelect={() => setPicked(p.name)}
          />
        ))}
      </div>
      <p className="status">Selected plan: {picked}</p>
    </>
  );
}
React plan picker after clicking Business: the Business card has the blue is-selected class and the status line reads Selected plan: Business
After one real click, Business carries is-selected and Starter lost it.

Checkpoint: click any active card and the blue ring moves to it. React re-renders, rebuilds each class string, and only the clicked card’s string contains is-selected.

Pro Tip: don’t reach for element.classList.add() in a click handler. React owns className, so the next render quietly overwrites anything you added by hand.

Step 4: Pass a className Prop to a Custom Component

Here’s the part that confuses people. className on a <div> works because it’s a DOM element. On your own <PlanCard /> it’s just a prop, and the component decides what to do with it.

TypeScript makes that visible. I added a small CurrentPlan.tsx that passes className to a card that doesn’t declare it:

type Plan = { name: string; price: number };

function PlanCard({ plan }: { plan: Plan }) {
  return <div className="plan-card">{plan.name}</div>;
}

export function CurrentPlan() {
  const team: Plan = { name: "Team", price: 29 };
  return <PlanCard plan={team} className="compact" />;
}

tsc output:

src/CurrentPlan.tsx(9,32): error TS2322: Type '{ plan: Plan; className: string; }' is not assignable to type 'IntrinsicAttributes & { plan: Plan; }'.
  Property 'className' does not exist on type 'IntrinsicAttributes & { plan: Plan; }'.
VS Code showing the TS2322 error because className does not exist on the PlanCard props type, with tsc failing in the terminal
Strict TypeScript rejects a className the component never declared.

Plain JavaScript would accept it and silently drop the class. The fix is the same in both: declare className as an optional prop and merge it into the component’s own list:

type CardProps = {
  plan: Plan;
  selected?: boolean;
  onSelect?: () => void;
  className?: string;
};

function PlanCard({ plan, selected, onSelect, className }: CardProps) {
  const classes = [
    "plan-card",
    className,
    plan.popular && "is-popular",
    plan.retired && "is-retired",
    selected && "is-selected",
  ].filter(Boolean).join(" ");
  // ...same <button> as Step 3
}

// inside App():
<aside>
  <h4>Your current plan</h4>
  <PlanCard plan={plans[1]} className="compact" selected />
</aside>

The sidebar card’s class list:

"plan-card compact is-popular is-selected"
React page with three plan cards and a compact Team card in a Your current plan sidebar that received the compact class from its parent
The parent passed compact; the card merged it with its own classes.

Checkpoint: the sidebar card is smaller than the others, and its class list starts with plan-card compact. The parent controls layout and the card still controls its own states.

Step 5: Clean Up the Class List with clsx

The array-and-join pattern works, but it gets noisy. clsx is a tiny package that takes strings, objects and falsy values and returns one clean string. Each object key is a class, kept only when its value is truthy.

Here’s the new PlanCard, plus an effect in App that logs the selected card’s classes. The VS Code shot below has the whole file:

import clsx from "clsx";

function PlanCard({ plan, selected, onSelect, className }: CardProps) {
  return (
    <button
      type="button"
      id={`plan-${plan.name}`}
      className={clsx("plan-card", className, {
        "is-popular": plan.popular,
        "is-retired": plan