In the module's first lesson we said a component encapsulates three things: markup, logic, and style. The first two are already settled: markup lives in the JSX and logic in the function body. Style, on the other hand, is still piled up in a single src/index.css that keeps growing unchecked, where nothing stops two different people from defining a .card class with incompatible rules three months from now. React imposes no single solution for this: it offers you several strategies and expects you to choose with judgment. In this lesson you'll see all four used in practice, all applied to the same component — BikeCard — so you can really compare them, you'll learn to build dynamic class names from a bike's status, you'll tidy up CicloUrbano's palette with CSS variables, and you'll adopt the strategy the project will use from here on.
Contents
- The problem: style is part of the component too
- Strategy 1: imported global CSS
- Strategy 2: inline styles with objects
- Strategy 3: CSS Modules with Vite
- Strategy 4: CSS-in-JS and utility frameworks
- Conditional classes based on a bike's status
- CSS variables for CicloUrbano's palette
- Final comparison and the course's decision
- The problem: style is part of the component too
Today BikeCard is incomplete as a unit: its markup and logic live in BikeCard.jsx, but its style rules sit a hundred lines away, in a file it shares with everything else. That creates four concrete problems:
| Problem | How it shows up |
|---|---|
| Name collisions | All the CSS lives in a global namespace. A .card class in two places steps on itself; whichever loads last wins |
| Orphaned styles | When a component is deleted, no one remembers to delete its CSS. The global file only grows |
| Fear of changing anything | Can I change .status? Nobody knows how many components use it. People end up adding .status-new instead of touching the existing one |
| No dynamism | CSS doesn't know the data. Painting a border in the bike type's color requires the component to get involved |
React doesn't solve this for you, but it does give you the missing ingredient: the component can decide its classes at run time, because className is a JavaScript expression like any other.
The four existing strategies can be ordered by where the style lives:
flowchart TD
A["Where does the style live?"] --> B["In a global .css<br/>(Strategy 1)"]
A --> C["In a JS object inside the JSX<br/>(Strategy 2)"]
A --> D["In a .module.css next to the component<br/>(Strategy 3)"]
A --> E["In the JSX itself as utility classes<br/>or generated by JS (Strategy 4)"]
- Strategy 1: imported global CSS
This is what you've used so far: write plain CSS and import it.
/* src/index.css */
.bike-card {
background: #ffffff;
border: 1px solid #d9e2ec;
border-radius: 8px;
padding: 1rem 1.25rem;
margin-bottom: 1rem;
max-width: 22rem;
}
.bike-card h3 {
margin-bottom: 0.5rem;
color: #12805c;
}// src/components/BikeCard.jsx
function BikeCard({ bike }) {
return (
<article className="bike-card">
<h3>{bike.model}</h3>
</article>
);
}Vite also lets you import a .css from the component itself, which improves the organization a little:
But watch out: this doesn't make it local. Vite collects every imported .css file and bundles them into the page's stylesheet. The file is closer to the component, yes, but its classes are still global.
The collision problem, with a real example
Imagine that two months from now someone adds a bookings screen with its own stylesheet:
/* src/components/BookingRow.css */
.bike-card { /* reuses the name "because it looks similar" */
max-width: 100%;
border: none;
background: #f5f7fa;
}That file gets imported and, without anyone intending it, the catalogue cards lose their border and max width. There's no error, no warning: just a broken layout that shows up when you navigate in a certain order. Debugging it takes half an afternoon.
The traditional mitigations exist and work halfway:
- BEM-style naming conventions:
.bike-card__title,.bike-card--urbana. They cut the risk a lot, but depend on the whole team's discipline, forever. - Per-component prefixes: the same idea, with the same fragility.
| Advantages | Drawbacks |
|---|---|
| Plain CSS, nothing to learn | Global namespace: collisions are guaranteed long-term |
| Works in any project | Style doesn't travel with the component |
| Ideal for resets, typography, and variables | No one dares delete rules: the file only grows |
When to use it: for what must be global. Resets (box-sizing), base typography, body colors, and above all the palette's CSS variables, as you'll see in section 7.
- Strategy 2: inline styles with objects
React lets you pass a JavaScript object to the style attribute:
function BikeCard({ bike }) {
return (
<article
style={{
background: '#ffffff',
border: '1px solid #d9e2ec',
borderRadius: '8px',
padding: '1rem 1.25rem',
maxWidth: '22rem'
}}
>
<h3 style={{ color: '#12805c', marginBottom: '0.5rem' }}>{bike.model}</h3>
</article>
);
}Remember from lesson 01-04 the rules for this object:
- Double braces: the outer ones open the JSX expression, the inner ones are the object literal.
- camelCase properties:
borderRadius, notborder-radius. - Values as strings, units included. Numbers without a unit are read as pixels:
padding: 16means16px.
Where it shines: values computed at run time
This is its legitimate case, and it's real. A station's occupancy bar, whose width depends on the data, can't be expressed with a CSS class, because the percentage is only known at run time:
// src/components/StationAvailability.jsx
/**
* Occupancy bar for a station.
* Props:
* - station (object, required) { id, name, district, docks }
* - occupied (number, optional, defaults to 0)
*/
function StationAvailability({ station, occupied = 0 }) {
const percentage = Math.round((occupied / station.docks) * 100);
return (
<div className="station-availability">
<p>
{station.name}: {occupied} of {station.docks} docks occupied
</p>
<div className="station-availability__bar">
{/* The width is only known at run time: a legitimate case for inline style */}
<div
className="station-availability__fill"
style={{ width: `${percentage}%` }}
/>
</div>
</div>
);
}
export default StationAvailability;Here the division of labor is right: the class supplies everything static (colors, height, borders) and the inline style supplies the one value that depends on the data.
Its limits, and they're severe
| Limitation | Consequence |
|---|---|
| No pseudo-classes | No :hover, :focus, :active, :disabled |
| No pseudo-elements | No ::before or ::after |
| No media queries | No responsive design |
| No descendant selectors | Every node has to be styled one by one |
| No animations | No @keyframes |
| Maximum specificity | An inline style beats almost everything; overriding it forces !important |
| A new object on every render | The {{…}} literal creates a different object each time, with implications for the optimizations in Module 8 |
| No caching or compression | The style travels in the JavaScript, not in a cacheable stylesheet |
When to use it: only for dynamic values that CSS can't know — percentage widths, positions, computed transforms — and always combined with a class that supplies the rest. Never as the main strategy.
- Strategy 3: CSS Modules with Vite
A CSS Module is an ordinary CSS file whose name ends in .module.css. Vite detects it automatically — no configuration needed — and does two things: it renames every class so it's unique across the whole application, and it hands you back an object that translates your names into the generated ones.
How it's written
/* src/components/BikeCard.module.css */
.card {
background: #ffffff;
border: 1px solid #d9e2ec;
border-radius: 8px;
padding: 1rem 1.25rem;
margin-bottom: 1rem;
max-width: 22rem;
border-left: 4px solid #d9e2ec;
}
.title {
margin: 0 0 0.5rem;
color: #12805c;
display: flex;
align-items: center;
gap: 0.5rem;
}
.meta {
margin: 0.25rem 0;
font-size: 0.95rem;
}
.price {
margin-top: 0.75rem;
font-weight: 700;
}
/* Variants by bike type */
.urbana { border-left-color: #12805c; }
.electrica { border-left-color: #b45309; }
.carga { border-left-color: #9b1c1c; }
/* Pseudo-classes, media queries, and everything else DO work here */
.card:hover {
box-shadow: 0 2px 8px rgba(31, 41, 51, 0.12);
}
@media (max-width: 30rem) {
.card {
max-width: 100%;
}
}Notice the names: .card, .title, .meta. Short and generic, because the local scope makes a prefix unnecessary. There's no risk of them colliding with the .card of another component.
How it's used
// src/components/BikeCard.jsx
import styles from './BikeCard.module.css';
import StatusBadge from './StatusBadge.jsx';
function BikeCard({ bike, stationName = 'Unknown station' }) {
const formattedPrice = bike.pricePerHour.toFixed(2);
return (
<article className={`${styles.card} ${styles[bike.type]}`}>
<h3 className={styles.title}>
{bike.model} <StatusBadge status={bike.status} />
</h3>
<p className={styles.meta}>Type: {bike.type}</p>
<p className={styles.meta}>Station: {stationName}</p>
<p className={styles.price}>€{formattedPrice} / hour</p>
</article>
);
}
export default BikeCard;What's happening underneath
import styles from './BikeCard.module.css' doesn't import text: it imports an object that Vite generates while processing the file.
// Approximate contents of the 'styles' object in development
{
card: '_card_1x9k2_1',
title: '_title_1x9k2_11',
meta: '_meta_1x9k2_18',
price: '_price_1x9k2_23',
urbana: '_urbana_1x9k2_28',
electrica: '_electrica_1x9k2_29',
carga: '_carga_1x9k2_30'
}And the HTML that reaches the browser:
<article class="_card_1x9k2_1 _urbana_1x9k2_28">
<h3 class="_title_1x9k2_11">Classic Urban <span class="status status--disponible">disponible</span></h3>
…
</article>That suffix is computed from the file's name and its contents, so it's impossible for two different components to generate the same class. The collision problem disappears by construction, not by discipline.
Two useful syntax details:
styles[bike.type]: bracket access. Sincebike.typeis'urbana','electrica', or'carga', we get the matching variant class. This is how data connects to style without a singleif.- Classes with hyphens: if you name a class
.featured-card, in JS you'll have to writestyles['featured-card']. That's why CSS Modules favor camelCase:.featuredCard→styles.featuredCard.
Advantages and drawbacks
| Advantages | Drawbacks |
|---|---|
| Local scope guaranteed by the tool | One extra file per component |
| It's real CSS: pseudo-classes, media queries, animations, everything | The generated names are unreadable when inspecting (configurable) |
| Style travels with the component: deleted along with it | Dynamic values still need inline styles or CSS variables |
| No dependencies: Vite ships with it | You have to remember styles. in front of every class |
| The browser can cache the resulting stylesheet | Sharing styles between components takes some thought (composes or a common module) |
This is CicloUrbano's default strategy from here on, for three reasons: it adds no dependency, it solves the real problem (collisions) without demanding discipline from anyone, and it's still CSS, so everything you know about the cascade still applies.
Styles shared across modules
When two components need the same rules, there are two clean routes:
/* src/styles/common.module.css */
.box {
background: #ffffff;
border: 1px solid #d9e2ec;
border-radius: 8px;
padding: 1rem 1.25rem;
}/* src/components/BikeCard.module.css */
.card {
composes: box from '../styles/common.module.css';
margin-bottom: 1rem;
max-width: 22rem;
}composes is a feature of its own in CSS Modules: the resulting class includes both. The simpler alternative is to put those shared rules in the global CSS and combine them with the helper from section 6: className={cx('box', styles.card)}.
- Strategy 4: CSS-in-JS and utility frameworks
The two remaining alternatives are very popular and worth knowing even though we won't use them in the course.
CSS-in-JS
Libraries like styled-components or Emotion let you write CSS inside JavaScript and get back an already-styled component:
// Example with styled-components (NOT used in this course)
import styled from 'styled-components';
const Card = styled.article`
background: #ffffff;
border: 1px solid #d9e2ec;
border-radius: 8px;
padding: 1rem 1.25rem;
/* Props reach the template: total dynamism */
border-left: 4px solid ${props =>
props.$type === 'electrica' ? '#b45309' : '#12805c'};
&:hover {
box-shadow: 0 2px 8px rgba(31, 41, 51, 0.12);
}
`;
<Card $type={bike.type}>…</Card>Its big appeal is that props determine the style directly, with the full power of CSS. Its drawbacks: one more dependency, a run-time cost (styles are generated while the app is running), friction with server rendering, and a clear ecosystem trend away from this approach toward solutions that resolve at build time (zero-runtime), like Vanilla Extract or Panda CSS.
Utility frameworks (Tailwind CSS)
The opposite approach: instead of writing CSS, you compose predefined atomic classes directly in the JSX.
// Example with Tailwind CSS (NOT used in this course)
<article className="bg-white border border-slate-200 rounded-lg p-4 mb-4 max-w-sm border-l-4 border-l-emerald-700 hover:shadow-md">
<h3 className="text-emerald-700 font-bold mb-2">{bike.model}</h3>
</article>Advantages: you never switch files to style something, the final CSS is tiny because only the utilities actually used get included, and the design system (spacing, colors, typography) is imposed for you, which gives a lot of consistency. Drawbacks: the JSX fills up with classes and loses readability, there's a learning curve for the names, and composing long conditionals needs a helper library.
The four, in perspective
| Global | Inline | CSS Modules | CSS-in-JS | Utilities | |
|---|---|---|---|---|---|
| Scope | Global | The element only | Local | Local | Global (atomic) |
| Pseudo-classes and media queries | Yes | No | Yes | Yes | Yes |
| Dynamism from props | No | Total | Via classes or variables | Total | Via classes |
| Dependencies | None | None | None (Vite) | A library | A tool |
| Run-time cost | None | Low | None | Medium | None |
- Conditional classes based on a bike's status
Whatever the strategy, there's a constant need: choosing classes based on the data. In CicloUrbano, the status badge must be green if the bike is available, orange if it's rented, and red if it's in maintenance.
With template strings
The most direct way, and the one you already used in StatusBadge:
// src/components/StatusBadge.jsx
function StatusBadge({ status = 'disponible' }) {
return <span className={`status status--${status}`}>{status}</span>;
}status--${status} produces status--disponible, status--alquilada, or status--mantenimiento. One single line covers all three cases, and adding a fourth status needs no change to the component, only to the CSS.
Watch out for one important limit: this works with global CSS, but not with CSS Modules, because a module's classes are renamed. There you have to go through the object:
import styles from './StatusBadge.module.css';
function StatusBadge({ status = 'disponible' }) {
return <span className={`${styles.badge} ${styles[status]}`}>{status}</span>;
}With conditionals
When the class depends on a boolean:
// Ternary: one condition
<article className={featured ? 'bike-card featured' : 'bike-card'}>
// With && inside the template: careful, if it's false it inserts "false"
<article className={`bike-card ${featured ? 'featured' : ''}`}>This turns unreadable fast. With three or four conditions you get monstrous templates, double spaces, and undefined sneaking into the attribute.
A small helper
The solution is a function that joins the valid names and drops the rest. It's three lines long and you'll use it in every project you write:
// src/utils/classNames.js
/**
* Joins class names, discarding falsy values.
* cx('card', false, undefined, 'active') -> 'card active'
*/
export function cx(...names) {
return names.filter(Boolean).join(' ');
}How it works, step by step:
...namesis a rest parameter: it collects every argument into an array..filter(Boolean)drops the falsy values from the array:false,undefined,null,'', and0. It's an idiomatic shortcut equivalent to.filter(value => Boolean(value))..join(' ')joins what's left with spaces, which is exactly the formatclassNameexpects.
With it, the JSX becomes readable again:
// src/components/BikeCard.jsx
import { cx } from '../utils/classNames.js';
import styles from './BikeCard.module.css';
import StatusBadge from './StatusBadge.jsx';
function BikeCard({ bike, stationName = 'Unknown station', featured = false }) {
const formattedPrice = bike.pricePerHour.toFixed(2);
const unavailable = bike.status !== 'disponible';
return (
<article
className={cx(
styles.card,
styles[bike.type],
featured && styles.featured,
unavailable && styles.dimmed
)}
>
<h3 className={styles.title}>
{bike.model} <StatusBadge status={bike.status} />
</h3>
<p className={styles.meta}>Type: {bike.type}</p>
<p className={styles.meta}>Station: {stationName}</p>
<p className={styles.price}>€{formattedPrice} / hour</p>
</article>
);
}
export default BikeCard;If featured is false, the expression featured && styles.featured is false, and filter(Boolean) drops it. No ternaries, no empty strings, no stray spaces.
In real projects you'll see the clsx library (or classnames), which does the same thing with more features. Our three-line helper covers the common case without adding dependencies.
And the classes missing from the module:
/* Add to src/components/BikeCard.module.css */
.featured {
box-shadow: 0 0 0 2px #12805c;
}
.dimmed {
opacity: 0.7;
}
- CSS variables for CicloUrbano's palette
CicloUrbano's colors have been repeating literally since lesson 01-03: #12805c in five places, #d9e2ec in just as many. Changing the brand green today would mean a find-and-replace across the whole project, risking missing one.
CSS variables (custom properties) solve this natively, with no tools or preprocessors:
/* src/index.css — at the top of the file */
:root {
/* Brand palette */
--color-brand: #12805c;
--color-rented: #b45309;
--color-maintenance: #9b1c1c;
/* Surfaces and text */
--color-bg: #f5f7fa;
--color-surface: #ffffff;
--color-text: #1f2933;
--color-border: #d9e2ec;
/* Shapes and spacing */
--radius: 8px;
--shadow: 0 2px 8px rgba(31, 41, 51, 0.12);
--space: 1rem;
}And now any style file in the project can use them, including .module.css files, because variables do cross the local scope: what CSS Modules renames are classes, not custom properties.
/* src/components/BikeCard.module.css */
.card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
padding: var(--space) 1.25rem;
border-left: 4px solid var(--color-border);
}
.card:hover {
box-shadow: var(--shadow);
}
.title {
color: var(--color-brand);
}
.urbana { border-left-color: var(--color-brand); }
.electrica { border-left-color: var(--color-rented); }
.carga { border-left-color: var(--color-maintenance); }Concrete advantages:
| Advantage | Example |
|---|---|
| A single source of truth | Changing --color-brand repaints the whole application |
| Can be changed at run time | A dark theme is just a matter of redefining the variables under another selector |
| Work with CSS Modules | The local scope doesn't affect custom properties |
| A perfect bridge to JS | An inline style can set a variable, and the CSS consumes it |
That last point deserves an example, because it combines the best of strategies 2 and 3:
// The component computes the value and hands it over as a CSS variable
<div
className={styles.fill}
style={{ '--fill-width': `${percentage}%` }}
/>.fill {
width: var(--fill-width, 0%);
height: 100%;
background: var(--color-brand);
transition: width 0.3s ease; /* the transition IS possible: it's real CSS */
}JavaScript only supplies the data; all the presentation — including the transition, impossible in an inline style — stays in the CSS. It's the recommended pattern for dynamic styles.
- Final comparison and the course's decision
| Criterion | Global CSS | Inline styles | CSS Modules | CSS-in-JS | Utilities (Tailwind) |
|---|---|---|---|---|---|
| Scope | Global | That element only | Automatic local | Automatic local | Global atomic |
| Collision risk | High | None | None | None | None |
Pseudo-classes and :hover |
Yes | No | Yes | Yes | Yes |
| Media queries | Yes | No | Yes | Yes | Yes |
| Animations | Yes | No | Yes | Yes | Yes |
| Dynamism with data | Via classes only | Total | Classes + CSS variables | Total | Conditional classes |
| Tools needed | None | None | None, with Vite | A library | A build tool |
| Run-time cost | None | Low | None | Medium | None |
| Style travels with the component | No | Yes | Yes | Yes | Yes |
| Learning curve | None | Low | Low | Medium | Medium |
| When to use it | Resets, typography, variables | Values computed at run time | Component styling: the default | Design systems with heavy dynamism | Teams wanting consistency and speed |
The combination CicloUrbano adopts
It isn't "pick one and drop the rest": the strategies complement each other, and this split is what you'll see in most Vite projects:
flowchart TD
A["src/index.css<br/>GLOBAL"] --> A1["Reset, typography,<br/>palette variables"]
B["Component.module.css<br/>CSS MODULES"] --> B1["All of the component's style:<br/>layout, colors, hover, media queries"]
C["style={{ }}<br/>INLINE"] --> C1["Only computed values,<br/>preferably as a CSS variable"]
Resulting file tree:
src/ ├── components/ │ ├── BikeCard.jsx │ ├── BikeCard.module.css <- style lives next to the component │ ├── StatusBadge.jsx │ ├── StatusBadge.module.css │ └── … ├── styles/ │ └── common.module.css <- rules shared across modules ├── utils/ │ └── classNames.js <- the conditional-class helper ├── data/ │ └── domain.js ├── App.jsx ├── index.css <- reset + global variables └── main.jsx
With this, BikeCard is finally a complete unit: its markup, its logic, and its style are in the same folder, under the same name, and can be deleted together without leaving a trace. That was the promise from the module's first lesson.
Common Mistakes and Tips
- Using
classinstead ofclassName. The most repeated React mistake.classis a reserved JavaScript word. - Forgetting the
.modulein the file name.BikeCard.cssis global; onlyBikeCard.module.cssturns on the local scope. The symptom is thatimport stylescomes back empty and the classes never apply. - Writing
className={styles}instead ofclassName={styles.card}.stylesis an object; React ends up painting[object Object]as the class. - Referring to a class that doesn't exist in the module.
styles.cradisundefinedand the element ends up with no class, no error at all. It's the hardest bug to spot: always check the generated HTML in the inspector. - Writing hyphenated CSS in a module and accessing it with a dot.
.featured-cardforcesstyles['featured-card']. Use camelCase in modules. - Inserting
falseinto a class template.`card ${featured && 'featured'}`produces literally"card false"when the condition is false. Use thecxhelper or a ternary with an empty string. - Putting units where React already adds them, or leaving them out where they're needed. In
style,width: 300means300px, butwidth: '80%'needs to be a string. Andflex: 1orzIndex: 10go without a unit. - Trying
:hoverwith inline styles. It doesn't exist. If you need it, you need a class. - Tip: start with the CSS variables. Before writing a project's first component, define the palette in
:root. It saves a guaranteed refactor. - Tip: name a module's classes by role, not by appearance.
.titleand.priceage well;.greenTextdoesn't. - Tip: if a component needs more than three or four conditional classes, review its design. It's usually a sign that two separate components are needed.
- Note on accessibility: color must never be the only carrier of information. Our
StatusBadgegets this right by also showing the status text next to the color. This is covered in depth in Accessibility in Interactive Components.
Exercises
Exercise 1
Migrate StatusBadge from global CSS to CSS Modules. Create src/components/StatusBadge.module.css with a base class and one class per status (disponible, alquilada, mantenimiento), using the palette's CSS variables. Adapt the component so it combines the base class with the one for the status it receives via props, and add a :hover effect that slightly increases the opacity.
Hint: the status class has to come from the styles object, not be built with a template string.
Exercise 2
Build the complete StationAvailability component (src/components/StationAvailability.jsx and its CSS module). It should receive a station from the domain and a number of occupied docks, and show the name, the count, and a progress bar. The bar's width must be passed as a CSS variable from the component, and the color must change with occupancy: green below 60%, orange between 60% and 85%, red above that.
Use the cx helper to choose the color class.
Exercise 3
For each situation, say which styling strategy you'd choose and why. Justify it in two or three sentences.
- The margin reset and
box-sizingfor the whole application. - The background color of a table row that depends on the bike's status (three possible values).
- The exact position of a tooltip, computed from the mouse coordinates.
- A reusable brand button with
:hover,:focus, and:disabledstates. - CicloUrbano's corporate color palette.
Solutions
Solution 1.
/* src/components/StatusBadge.module.css */
.badge {
display: inline-block;
padding: 0.15rem 0.6rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
vertical-align: middle;
color: var(--color-surface);
opacity: 0.92;
transition: opacity 0.2s ease;
}
.badge:hover {
opacity: 1;
}
.disponible { background-color: var(--color-brand); }
.alquilada { background-color: var(--color-rented); }
.mantenimiento { background-color: var(--color-maintenance); }// src/components/StatusBadge.jsx
import { cx } from '../utils/classNames.js';
import styles from './StatusBadge.module.css';
/**
* Visual badge for a bike's status.
* Props:
* - status (string, optional, defaults to 'disponible'):
* 'disponible' | 'alquilada' | 'mantenimiento'
*/
function StatusBadge({ status = 'disponible' }) {
return (
<span className={cx(styles.badge, styles[status])}>{status}</span>
);
}
export default StatusBadge;The key point: className={`status status--${status}`} stops working with modules, because the classes are renamed. You have to look them up in the object with styles[status]. And if an unexpected status arrived, styles[status] would be undefined and the cx helper would drop it without breaking anything: the badge would render with no background color instead of failing.
Solution 2.
/* src/components/StationAvailability.module.css */
.container {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
padding: var(--space) 1.25rem;
margin-bottom: var(--space);
max-width: 32rem;
}
.text {
margin: 0 0 0.5rem;
font-size: 0.95rem;
}
.bar {
height: 10px;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 999px;
overflow: hidden;
}
.fill {
/* The width arrives as a CSS variable from the component */
width: var(--occupancy, 0%);
height: 100%;
transition: width 0.3s ease;
}
.low { background: var(--color-brand); }
.medium { background: var(--color-rented); }
.high { background: var(--color-maintenance); }// src/components/StationAvailability.jsx
import { cx } from '../utils/classNames.js';
import styles from './StationAvailability.module.css';
/**
* Occupancy of a CicloUrbano station.
* Props:
* - station (object, required) { id, name, district, docks }
* - occupied (number, optional, defaults to 0)
*/
function StationAvailability({ station, occupied = 0 }) {
const percentage = Math.round((occupied / station.docks) * 100);
const level = percentage < 60 ? 'low' : percentage <= 85 ? 'medium' : 'high';
return (
<div className={styles.container}>
<p className={styles.text}>
{station.name} ({station.district}): {occupied} of {station.docks} docks
occupied · {percentage}%
</p>
<div className={styles.bar}>
<div
className={cx(styles.fill, styles[level])}
style={{ '--occupancy': `${percentage}%` }}
/>
</div>
</div>
);
}
export default StationAvailability;// src/App.jsx (excerpt)
<StationAvailability station={stations[0]} occupied={11} />
<StationAvailability station={stations[1]} occupied={13} />
<StationAvailability station={stations[2]} occupied={9} />Division of responsibilities: the component computes only the data (percentage and level), the CSS module supplies all the presentation — transition included — and the inline style is limited to handing over a variable. Both percentage and level are derived values: neither is state, as you studied in the previous lesson.
Solution 3.
| Case | Strategy | Justification |
|---|---|---|
1. Reset and box-sizing |
Global CSS | It's global by nature: it affects every element in the document. Scoping it per component wouldn't make sense |
| 2. Row background based on status | CSS Modules with a conditional class | Three values known ahead of time: three classes and a styles[status]. No run-time dynamism is needed |
| 3. Tooltip position | Inline style (or CSS variable) | The coordinates are only known at run time and form a continuum, not a set of cases. This is the legitimate case for inline style |
4. Brand button with :hover, :focus, and :disabled |
CSS Modules | Pseudo-classes are impossible inline, and the local scope keeps another component's .button from stepping on it |
| 5. Corporate palette | CSS variables in :root (global) |
A single source of truth, reachable from any module, and changeable at run time for alternate themes |
Conclusion
You now know how to style a React component with judgment, not out of habit. You've seen all four strategies on the same BikeCard: global CSS, convenient but with a shared namespace where collisions are only a matter of time; inline styles, unbeatable for values computed at run time and useless for everything else — no :hover, no media queries, no animations; CSS Modules, which Vite supports out of the box and which solve local scope by construction; and the ecosystem's alternatives, CSS-in-JS and utility frameworks, with their trade-offs clearly identified.
CicloUrbano adopts the combination that strikes the best balance: CSS variables in :root for the palette and shapes, a .module.css next to every component for all of its style, and inline styles only to hand over computed values, preferably as a CSS variable so the presentation keeps living in the CSS. Along the way you've picked up two tools you'll use constantly: building classes from data with styles[bike.type], and composing conditional classes with the cx helper, which drops falsy values without cluttering the JSX.
With this you close out Module 2. You know what a component is as a unit of design and how to draw its boundaries; you know how to read legacy code written with classes and why everything new gets written with functions; you know how to parameterize a component with read-only props and compose content with children; you know how to give it memory with state, update it immutably, and tell what should be stored apart from what should be derived; and now you know how to dress it without the style spiraling out of control. BikeCard is, at last, a complete unit: markup, logic, and style in the same folder.
But the catalogue is still just a showcase. TypeSelector remembers the chosen type and filters nothing; DockCounter subtracts docks that matter to no one else; there are no forms, no validation, and lists are still written card by card. Real interaction is missing. In Module 3: Working with Events you'll learn to handle React events with all their rules, to show or hide parts of the interface with conditional rendering, to paint whole collections with map and finally understand what the keys you saw in reconciliation are for, to build controlled forms and validate them, and to make all of it accessible. The next lesson is Handling Events in React, and from there CicloUrbano will stop just looking at itself and start responding.
React Course
Module 1: Getting Started with React
- What Is React?
- Setting Up the Development Environment
- Hello World in React
- JSX: A JavaScript Syntax Extension
- How React Renders: Virtual DOM and Reconciliation
Module 2: React Components
- Understanding Components
- Function vs Class Components
- Props: Passing Data to Components
- State: Managing Component State
- Styling Components: CSS, Modules and Utilities
Module 3: Working with Events
- Handling Events in React
- Conditional Rendering
- Lists and Keys
- Forms and Controlled Components
- Form Validation and Uncontrolled Components
- Accessibility in Interactive Components
Module 4: Advanced Component Concepts
- Lifting State Up
- Composition vs Inheritance
- React Lifecycle Methods
- Hooks: Introduction and Basic Use
- Error Boundaries: Catching Failures in the UI
Module 5: React Hooks
- The useState Hook
- The useEffect Hook
- The useRef Hook and DOM Access
- The useContext Hook
- The useReducer Hook
- Custom Hooks
Module 6: Routing in React
- Introducing React Router
- Setting Up React Router
- Nested Routes
- Programmatic Navigation
- Protected Routes and Access Control
Module 7: State Management
- Introduction to State Management
- The Context API
- Redux: Introduction and Setup
- Redux: Actions and Reducers
- Redux: Connecting to React
- Server State: Fetching, Caching and Syncing
Module 8: Performance Optimization
- Performance Optimization Techniques in React
- Memoization with React.memo
- The useMemo and useCallback Hooks
- Code Splitting and Lazy Loading
- Measuring Performance with React DevTools Profiler
Module 9: Testing React Applications
- Introduction to Testing
- Unit Testing with Jest
- Component Testing with React Testing Library
- Testing Asynchronous Code and Mocking APIs
- End-to-End Testing with Cypress
Module 10: Advanced Topics
- Server-Side Rendering (SSR) with Next.js
- Static Site Generation (SSG) with Next.js
- Suspense and React Server Components
- TypeScript with React
- React Native: Building Mobile Apps
