A page that works but looks bad is not a good page. React does not force one way to style. You choose. In this lesson we look at the four most common ways, and when each one is best. We also learn how to change styles when state changes, how to make a page work on phones, and how to add dark mode.
Way 1: a plain CSS file
The simplest way. Write a normal CSS file, import it, and use className on your elements.
/* Button.css */
.btn {
background: #2563eb;
color: white;
border: none;
padding: 10px 16px;
border-radius: 8px;
cursor: pointer;
}
.btn:hover {
background: #1d4ed8;
}
import './Button.css';
function Button({ children }) {
return <button className="btn">{children}</button>;
}
This works well for small projects. The weakness: CSS is global. A .btn rule affects every .btn in the whole app, even in other files. In a big project two people can use the same class name by accident, and styles clash.
Way 2: CSS Modules
CSS Modules fix the clash. Name your file with .module.css. Vite then changes each class name to a unique one, so the style only applies where you import it.
/* Card.module.css */
.card {
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 16px;
}
.title {
font-size: 1.25rem;
}
import styles from './Card.module.css';
function Card({ title, children }) {
return (
<section className={styles.card}>
<h2 className={styles.title}>{title}</h2>
{children}
</section>
);
}
styles is an object, and styles.card is the unique class name. You write normal CSS, and you get safety for free. This is a very good default for apps of any size.
Way 3: inline styles
You met these in the JSX lesson. The style prop takes an object with camelCase names.
<div style={{ width: progress + '%', background: 'seagreen', height: 8 }} />
Inline styles are best for values that change often or come from data, like the width of a progress bar. They cannot do hover effects, media queries or other pseudo-classes, so do not use them for everything.
Way 4: Tailwind CSS
Tailwind is a popular tool that gives you many small ready-made classes, such as p-4 (padding) and text-lg (large text). You build a design by putting the classes right in your JSX, and you rarely write a CSS file.
function Alert({ children }) {
return (
<div className="rounded-lg border border-amber-300 bg-amber-50 p-4 text-amber-900">
{children}
</div>
);
}
Many people like Tailwind because it is fast to work with and keeps design consistent. Others find long class lists hard to read. Both views are fair. To add it to a Vite project, follow the install guide on the Tailwind website, since the steps change between versions.
Which one should you choose?
- Learning and small projects: plain CSS or CSS Modules.
- Team apps that must not clash: CSS Modules.
- Fast building with a consistent look: Tailwind.
- Values from data (widths, colours): a small inline style on top of any of the above.
You can mix them. Many projects use Tailwind for most things, and an inline style for one special value. The most important thing is to be consistent inside one project.
Changing styles with state
Styles are just strings in className, so you can change them with state and a ternary:
function Tab({ active, children, onClick }) {
return (
<button className={active ? 'tab tab-active' : 'tab'} onClick={onClick}>
{children}
</button>
);
}
With several conditions the string gets messy. A tiny helper called clsx joins class names and ignores false values:
import clsx from 'clsx';
<button className={clsx('btn', isPrimary && 'btn-primary', disabled && 'btn-disabled')}>
Save
</button>
Responsive design: mobile first
Many visitors use a phone. Write the styles for a small screen first, then add rules for bigger screens with min-width media queries.
.grid {
display: grid;
gap: 16px;
grid-template-columns: 1fr; /* phone: one column */
}
@media (min-width: 700px) {
.grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1000px) {
.grid { grid-template-columns: repeat(3, 1fr); }
}
In Tailwind the same idea is written with prefixes: grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3. Test your page by making the browser window narrow, or use the device mode in the browser tools. Make sure nothing makes the page scroll sideways at a width of about 390 pixels.
- Use relative sizes (
rem,%,fr) rather than fixed pixel widths for layout. - Set
max-width: 100%on images so they never overflow. - Make tap targets, such as buttons, at least about 44 pixels high.
Dark mode
Many devices let people choose a dark theme. You can follow that choice with one media query and CSS variables:
:root {
--bg: #ffffff;
--text: #0f172a;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f172a;
--text: #e2e8f0;
}
}
body {
background: var(--bg);
color: var(--text);
}
CSS variables (custom properties) are very useful. You change colours in one place, and the whole app follows. Always check colour contrast, so text is easy to read in both themes.
Helpful habits
- Keep one global file for the basics: fonts, colours as variables, and a simple reset.
- Put component styles next to the component, for example
Card.jsxandCard.module.css. - Do not use
!importantunless you have no other choice. - Do not hide the keyboard focus outline. If you change it, make it clearly visible.
- Use semantic HTML (
button,nav,main) first, and style it second.
Common mistakes
- Using
classinstead ofclassName. - Importing a CSS Module but writing a plain string:
className="card"instead ofclassName={styles.card}. - Expecting a plain CSS file to be limited to one component. It is global.
- Using fixed pixel widths that break on phones.
- Using inline styles for hover effects, which they cannot do.
A small example that uses everything
Here is a pricing card that is responsive, has a state-based style and supports dark mode through CSS variables. The CSS file uses a CSS Module:
/* Plan.module.css */
.plan {
border: 1px solid var(--line);
border-radius: 12px;
padding: 16px;
background: var(--bg);
color: var(--text);
}
.featured {
border-color: #2563eb;
box-shadow: 0 4px 16px rgba(37, 99, 235, 0.2);
}
import styles from './Plan.module.css';
import clsx from 'clsx';
function Plan({ name, price, featured }) {
return (
<article className={clsx(styles.plan, featured && styles.featured)}>
<h3>{name}</h3>
<p>{price} per month</p>
</article>
);
}
The card uses CSS variables for its colours, so dark mode needs no extra code in the component. The featured style is added only when the prop is true. And because it is a CSS Module, the class names cannot clash with any other file. This is a good pattern to copy in your own projects.
Questions beginners often ask
Do I have to choose only one way? No. Many projects mix them, for example Tailwind for layout and a small CSS file for a special animation. Just keep it consistent so other people can follow it.
Why does my CSS Module class not work? Check that the file ends with .module.css, that you imported it as an object, and that you wrote styles.name and not a string. Class names with dashes need brackets: styles["my-class"].
How can I use a font? Add the font files or a link in index.html, and set font-family on body in your global CSS. Always set a fallback font, such as system-ui, sans-serif.
How do I add animation? Start with plain CSS transitions, like transition: background 0.2s, for hover and focus changes. Respect people who ask for less motion with prefers-reduced-motion.
Your task
- Make a
Cardcomponent styled with a CSS Module: border, padding, rounded corners. - Make a
Tabrow of three buttons where the clicked tab is highlighted using state. - Make a grid of six cards that shows one column on a phone and three on a wide screen.
- Add dark mode with CSS variables and check the text is easy to read in both themes.
How to check it worked
- No sideways scrolling at 390 pixels wide.
- The chosen tab changes colour when clicked.
- Switching your device to dark mode changes the page colours.
Summary
- Four ways to style: plain CSS, CSS Modules, inline styles and Tailwind. Pick one main way per project.
- CSS Modules give each component its own class names, so styles never clash.
- Use state and a ternary (or
clsx) to change classes. Use inline styles only for values from data. - Design for phones first, use CSS variables, and support dark mode.