Web Development with React
Hands-onBeginner 35 min· Building real apps · 13 of 21

Styling: CSS modules and Tailwind CSS

Learn two popular ways to style React components: CSS Modules, which scope plain CSS to one component, and Tailwind CSS, which uses small utility classes. You will style a Brew & Bean menu card both ways and learn when to pick which.

In this tutorial you will style the same Brew & Bean menu card twice: first with CSS Modules, then with Tailwind CSS. Seeing both side by side makes the trade-offs clear.

Prerequisites: a Vite + React project, props and lists, and the basics of CSS (selectors, margin, padding, flexbox; see *HTML and CSS essentials*). Plain JavaScript (JSX).

The problem with global CSS

Normal CSS is global. If one file defines .title { color: red; }, every element with the class title anywhere in the app turns red. In a big app with many developers, class names collide and a change in one place breaks another. Everything in this lesson is a way to avoid that.

Ways to style React

ApproachHow it worksGood for
Plain CSS fileOne global stylesheetResets, fonts, a few global rules
Inline style propstyle={{ color: 'red' }} object on the elementValues computed at run time, such as a progress bar width
CSS ModulesNormal CSS file whose class names are made unique per fileComponent-level styles with the CSS you already know
Tailwind CSSTiny single-purpose classes in the markupFast building, consistent spacing and colours

CSS Modules

A CSS Module is any file named *.module.css. Vite supports it without any setup. When you import it, you get an object that maps your class names to generated unique ones such as MenuCard_title__x7Qa2. Nothing leaks out and nothing else leaks in.

src/MenuCard.module.css
.card {
  border: 1px solid #e5d9cc;
  border-radius: 12px;
  padding: 16px;
  background: #fffaf3;
  max-width: 280px;
}

.title {
  margin: 0 0 4px;
  font-size: 1.2rem;
  color: #4a2c17;
}

.category {
  font-size: 0.8rem;
  text-transform: uppercase;
  color: #8a6d57;
}

.price {
  margin-top: 12px;
  font-weight: 700;
}

.badge {
  display: inline-block;
  margin-left: 8px;
  padding: 2px 8px;
  border-radius: 999px;
  background: #4a2c17;
  color: white;
  font-size: 0.7rem;
}

.soldOut {
  opacity: 0.5;
}

Ordinary CSS. Use camelCase class names such as soldOut so you can write styles.soldOut in JavaScript (styles['sold-out'] also works).

src/MenuCard.jsx (CSS Modules version)
import styles from './MenuCard.module.css';

function MenuCard({ name, category, price, soldOut = false }) {
  const cardClass = soldOut ? styles.card + ' ' + styles.soldOut : styles.card;

  return (
    <article className={cardClass}>
      <h3 className={styles.title}>
        {name}
        {price >= 200 && <span className={styles.badge}>Premium</span>}
      </h3>
      <p className={styles.category}>{category}</p>
      <p className={styles.price}>₹{price}</p>
    </article>
  );
}

export default MenuCard;

styles.card is a string like 'MenuCard_card__a1b2c'. To combine classes, join them with a space; a small helper or the clsx package makes this tidier.

src/App.jsx (try the card)
import MenuCard from './MenuCard';

function App() {
  return (
    <div style={{ display: 'flex', gap: 16, padding: 16, flexWrap: 'wrap' }}>
      <MenuCard name="Espresso" category="coffee" price={120} />
      <MenuCard name="Cold Brew" category="coffee" price={220} />
      <MenuCard name="Banana Bread" category="bakery" price={130} soldOut />
    </div>
  );
}

export default App;
What you should see
Three cream-coloured rounded cards in a row (wrapping on a narrow screen). Cold Brew has a dark 'Premium' pill next to its name. Banana Bread looks faded. In DevTools, the class names on the elements are long generated names, not just `card`.

Global styles and :global

Keep resets and base styles (body font, box-sizing) in a normal file such as src/index.css. The default Vite template includes some demo styles in index.css and App.css that centre everything; delete what you do not need so it does not fight your styles.

Tailwind CSS

Tailwind gives you thousands of small classes, each doing one thing: p-4 is padding, rounded-xl rounds corners, text-lg sets font size. You build a design by combining them in className, without writing a CSS file. Tailwind scans your files and generates only the CSS you actually use, so the final file stays small.

It looks odd at first, like writing inline styles. The differences: classes come from a fixed scale (so spacing and colours stay consistent), they support hover, focus and screen-size variants (hover:bg-amber-100, md:flex), and there is no naming to invent.

Install Tailwind in a Vite project

  1. 1In the project folder run npm install tailwindcss @tailwindcss/vite.
  2. 2Open vite.config.js, import tailwindcss from @tailwindcss/vite and add tailwindcss() to the plugins array next to react().
  3. 3Replace the contents of src/index.css with the single line @import "tailwindcss";.
  4. 4Make sure src/main.jsx imports ./index.css (the Vite template already does).
  5. 5Restart npm run dev and use a Tailwind class on any element to check it works.
vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  plugins: [react(), tailwindcss()],
});

This is the setup for Tailwind version 4, current at the time of writing (2026). Version 3 used a tailwind.config.js file and PostCSS instead, so older tutorials look different. The official Tailwind docs have an install page for Vite.

src/index.css
@import "tailwindcss";

Tailwind 4 needs only this line. No config file is required to start.

src/MenuCard.jsx (Tailwind version)
function MenuCard({ name, category, price, soldOut = false }) {
  return (
    <article
      className={
        'w-70 rounded-xl border border-amber-200 bg-amber-50 p-4 shadow-sm ' +
        'hover:shadow-md ' +
        (soldOut ? 'opacity-50' : '')
      }
    >
      <h3 className="mb-1 text-lg font-semibold text-amber-950">
        {name}
        {price >= 200 && (
          <span className="ml-2 rounded-full bg-amber-950 px-2 py-0.5 text-xs text-white">
            Premium
          </span>
        )}
      </h3>
      <p className="text-xs uppercase text-amber-800">{category}</p>
      <p className="mt-3 font-bold">₹{price}</p>
    </article>
  );
}

export default MenuCard;

Same card, no CSS file. The class numbers are a scale: p-4 is 1rem of padding (4 x 0.25rem). Check the Tailwind docs if a class does not seem to work; names are the most common stumbling block.

You can read the card straight from the class list: padding, rounded corners, an amber border and background, a shadow that grows on hover. Notice the conditional class: just a string that is built with a ternary, like any other JavaScript.

Responsive design and states

Tailwind is mobile first. A class without a prefix applies to all screen sizes; a prefix such as md: applies from that width upwards. Most of your users in India are on phones, so start with the phone layout and add sm:, md: and lg: for bigger screens. States work the same way with prefixes: hover:, focus:, disabled:.

src/App.jsx (responsive grid)
import MenuCard from './MenuCard';

function App() {
  return (
    <main className="mx-auto max-w-4xl p-4">
      <h1 className="mb-4 text-2xl font-bold text-amber-950 md:text-4xl">
        Brew & Bean menu
      </h1>
      <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
        <MenuCard name="Espresso" category="coffee" price={120} />
        <MenuCard name="Cold Brew" category="coffee" price={220} />
        <MenuCard name="Banana Bread" category="bakery" price={130} soldOut />
      </div>
      <button className="mt-6 rounded-lg bg-amber-900 px-4 py-2 text-white hover:bg-amber-800 focus:outline-none focus:ring-2 focus:ring-amber-400 disabled:opacity-50">
        Order now
      </button>
    </main>
  );
}

export default App;

One column on phones, two from the sm breakpoint, three from lg. The w-70 class on the card fills a grid cell poorly, so change it to w-full when you use the grid (you will do this in the exercise).

See your phone layout

In Chrome or Edge press F12 and click the device toolbar icon (or Ctrl+Shift+M) to test phone widths such as 360 px, the size many Android phones use.

Dealing with long class lists

Long className strings are the usual complaint. The answer in React is not to move the classes to a CSS file but to make a component. If a button style is repeated, build a <Button> component that holds the classes once. Then you get the reuse that CSS classes would have given you.

src/Button.jsx
function Button({ children, variant = 'primary', ...props }) {
  const base = 'rounded-lg px-4 py-2 font-medium focus:outline-none focus:ring-2 disabled:opacity-50 ';
  const variants = {
    primary: 'bg-amber-900 text-white hover:bg-amber-800 focus:ring-amber-400',
    ghost: 'border border-amber-900 text-amber-900 hover:bg-amber-100 focus:ring-amber-400',
  };

  return (
    <button className={base + variants[variant]} {...props}>
      {children}
    </button>
  );
}

export default Button;

Use it as <Button>Order now</Button> or <Button variant="ghost" onClick={...}>Cancel</Button>. Tailwind must see complete class names in your source, so avoid building names like 'bg-' + colour at run time.

Do not build class names from pieces

Tailwind finds classes by scanning your source for whole strings. 'bg-' + color + '-500' never appears as a complete class, so the style is not generated and nothing changes on screen. Write the full names ('bg-red-500') in an object or ternary and choose between them.

Which should you choose?

CSS Modules vs Tailwind

QuestionCSS ModulesTailwind CSS
Learning curveLow if you know CSSNeed to learn the class names (they follow CSS closely)
Where styles liveSeparate .module.css fileIn the JSX markup
ConsistencyUp to your teamBuilt-in scale for spacing, colours and sizes
Responsive and hover statesWrite media queries yourselfPrefixes like md: and hover:
Good fitTeams that prefer plain CSS, small appsFast prototypes, design systems, most new projects

Both are widely used, and you can mix them in one project. Many job listings mention Tailwind, so learning it is worthwhile, but understanding CSS underneath is what makes you good at either one. If you cannot say what flex, p-4 or rounded-xl means in CSS, go back to the CSS lesson.

How this comes up in interviews: "How do you avoid style conflicts in a large React app?" (CSS Modules, utility classes, CSS-in-JS or naming conventions) and "What are the downsides of utility-first CSS?" (long class lists; solved by components).

Prompt · Ask AI to convert CSS to Tailwind
Convert this CSS to Tailwind CSS version 4 utility classes for a React component. Keep the same look, use mobile-first responsive prefixes where I describe them, and after the code give a two-column table of each CSS property and the Tailwind class that replaces it, so I can learn the mapping.

---
[paste your CSS and JSX]
---

What to expect: Check unfamiliar classes in the Tailwind docs; AI sometimes invents classes or uses version 3 syntax.

Try it: Style the order summary

1. Change the card width to w-full so it fills the grid and check it at 360 px and 1024 px widths. 2. Make a Badge component with variant props info and warning using Tailwind classes. 3. Rebuild the same card using CSS Modules instead (or the other way round) and compare the two files. 4. Add a dark mode: Tailwind's dark: prefix applies styles when the user's system is in dark mode (for example dark:bg-stone-900 dark:text-white). Style the page and card.

Key takeaways

  1. Global CSS causes name clashes in large apps; scoped approaches avoid them.
  2. CSS Modules (*.module.css) generate unique class names; import the file as styles and use styles.card.
  3. Tailwind uses small utility classes from a fixed scale, with prefixes for hover, focus and breakpoints; it is mobile first.
  4. In Tailwind 4 with Vite, install tailwindcss and @tailwindcss/vite, add the plugin and put @import "tailwindcss"; in your CSS.
  5. Reuse styles by making components, and never build Tailwind class names from string pieces.
  6. Use inline style only for run-time values; understand CSS first, whichever tool you choose.

Check yourself

Finishing the quiz marks the lesson as done.

1. What does a CSS Module do to your class names?

2. In Tailwind, what does md:flex mean?

3. Why does className={'bg-' + color + '-500'} often fail in Tailwind?

4. Your Tailwind button classes are repeated in 20 places. What is the React way to reduce the repetition?

5. When is the inline style prop the best choice?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free