When you start building a React application, styles feel like a minor detail. Just add a className, sprinkle some CSS — done. But as the app grows, the pain sets in: one button is blue, another is dark blue, on a third screen it's a different size altogether. Every developer styled things "their way," and now the UI looks fragmented.
That is why two decisions matter in frontend: how to isolate styles (so that one component's styles don't break another's) and where to store shared values (so that all buttons share the same color).
The problem with plain CSS
CSS in a browser is global. If you write .card { padding: 16px } in one file, that rule applies to every element with the class card across the entire application. When there are dozens of components, class names start clashing: someone accidentally overwrites another component's styles.
Several approaches were invented to avoid this.
CSS Modules
CSS Modules are ordinary CSS files, but at build time all class names are automatically made unique. Two different components can both have a class called .card, and they will never collide.
import styles from "./ProductCard.module.css";
export function ProductCard() {
return <article className={styles.card}>...</article>;
}
/* ProductCard.module.css */
.card {
padding: 1rem;
border-radius: 8px;
border: 1px solid #e2e8f0;
}
In the browser the class becomes something like ProductCard_card__3xKm7 — unique, nothing leaks out.
Pros: simplicity, no extra dependencies, close to the standard. Con: you have to switch between two files — the component and its styles.
Tailwind
Tailwind takes a different approach: instead of writing CSS you use small ready-made utility classes directly in your markup. p-4 means padding: 1rem, rounded-lg rounds the corners, border adds a border.
export function ProductCard() {
return (
<article className="rounded-lg border p-4">
...
</article>
);
}
There is no CSS file at all — everything is inline in JSX. Tailwind only generates the classes you actually used, so the final stylesheet stays small.
Pros: fast, no need to invent class names, a consistent scale of values (all spacing multiples of one unit). Con: markup gets longer, you need to get used to reading multi-line className strings.
CSS-in-JS
CSS-in-JS means styles are written directly in JavaScript and can depend on component props. Popular libraries: styled-components, Emotion.
const Card = styled.article<{ highlighted: boolean }>`
padding: 1rem;
border: 1px solid ${(p) => (p.highlighted ? "#2f5b4f" : "#e2e8f0")};
`;
Pro: styles can change dynamically based on state. Con: some libraries compute styles in the browser at runtime, which slows down rendering. With server-side rendering (Next.js) extra configuration is required. That is why CSS-in-JS is chosen more carefully in new projects than it used to be.
What to choose
There is no single right answer — it depends on the project and the team:
- CSS Modules — a safe choice for those comfortable with classic CSS. Works well in any project.
- Tailwind — great when you need to build UI fast and the team is on board with its syntax. Very popular in the 2020s.
- CSS-in-JS — when styles depend heavily on component state and that matters more than the runtime cost.
Design tokens
Whichever styling mechanism you choose, one question remains: where does the value 16px come from? Or #2f5b4f? If every component stores these values locally, sooner or later one button will be 14px, another 16px, a third 1rem — all "basically the same," but not actually.
Design tokens are named variables for all repeating values: colors, spacing, border radii, fonts. Declared in one place, used everywhere.
:root {
--color-accent: #2f5b4f;
--color-border: #e2e8f0;
--space-4: 1rem;
--radius-md: 8px;
}
.card {
padding: var(--space-4);
border-radius: var(--radius-md);
border: 1px solid var(--color-border);
}
Now if the designer decides to change the primary color — update it in one place and the change propagates across the entire application. If you remove tokens and hardcode #2f5b4f in every component, a color change means searching through the whole codebase.
In Tailwind, tokens live in the configuration file (tailwind.config.js). In plain CSS — in :root as custom properties. In CSS-in-JS — in a theme object. The mechanism differs, the principle is the same.
Shared components and consistency
Design tokens solve the values problem. But if every developer writes their own button, buttons will still be inconsistent. That is why tokens are complemented by shared UI primitives: Button, Input, Card, Badge.
When such a set exists, a new screen is assembled from ready-made building blocks that already use the same tokens, rather than being styled from scratch. This is what "design system" means in its minimal form — not necessarily a large library; sometimes a shared/ui folder with a dozen components is enough.
In short
- Plain CSS is global — CSS Modules and other approaches isolate styles per component.
- CSS Modules: ordinary CSS with local class names, no runtime — a good default choice.
- Tailwind: utility classes inline in markup, fast and consistent, takes getting used to.
- CSS-in-JS: styles depend on props, but there is a runtime cost — chosen deliberately.
- Design tokens — named variables for colors, spacing, and so on; change in one place, propagates everywhere.
- Shared UI components (
Button,Card,Input) plus tokens give a consistent UI without restyling every screen.
What to read next
- CSS in Depth — a series on the language itself: cascade, box model, flexbox and grid.
- Project structure — where to keep shared components and how to organize
shared/ui. - Components and props — the foundation of building UIs from independent blocks.
- Accessibility — how to make an interface accessible to all users.