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

  1. The problem: style is part of the component too
  2. Strategy 1: imported global CSS
  3. Strategy 2: inline styles with objects
  4. Strategy 3: CSS Modules with Vite
  5. Strategy 4: CSS-in-JS and utility frameworks
  6. Conditional classes based on a bike's status
  7. CSS variables for CicloUrbano's palette
  8. Final comparison and the course's decision

  1. 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)"]

  1. 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/main.jsx  — imported once, affects the whole application
import './index.css';
// 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:

// src/components/BikeCard.jsx
import './BikeCard.css';   // the file lives next to the component

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.

  1. 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, not border-radius.
  • Values as strings, units included. Numbers without a unit are read as pixels: padding: 16 means 16px.

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.

  1. 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. Since bike.type is 'urbana', 'electrica', or 'carga', we get the matching variant class. This is how data connects to style without a single if.
  • Classes with hyphens: if you name a class .featured-card, in JS you'll have to write styles['featured-card']. That's why CSS Modules favor camelCase: .featuredCardstyles.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)}.

  1. 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

  1. 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:

  • ...names is a rest parameter: it collects every argument into an array.
  • .filter(Boolean) drops the falsy values from the array: false, undefined, null, '', and 0. It's an idiomatic shortcut equivalent to .filter(value => Boolean(value)).
  • .join(' ') joins what's left with spaces, which is exactly the format className expects.

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;
}

  1. 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.

  1. 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 class instead of className. The most repeated React mistake. class is a reserved JavaScript word.
  • Forgetting the .module in the file name. BikeCard.css is global; only BikeCard.module.css turns on the local scope. The symptom is that import styles comes back empty and the classes never apply.
  • Writing className={styles} instead of className={styles.card}. styles is an object; React ends up painting [object Object] as the class.
  • Referring to a class that doesn't exist in the module. styles.crad is undefined and 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-card forces styles['featured-card']. Use camelCase in modules.
  • Inserting false into a class template. `card ${featured && 'featured'}` produces literally "card false" when the condition is false. Use the cx helper 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: 300 means 300px, but width: '80%' needs to be a string. And flex: 1 or zIndex: 10 go without a unit.
  • Trying :hover with 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. .title and .price age well; .greenText doesn'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 StatusBadge gets 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.

  1. The margin reset and box-sizing for the whole application.
  2. The background color of a table row that depends on the bike's status (three possible values).
  3. The exact position of a tooltip, computed from the mouse coordinates.
  4. A reusable brand button with :hover, :focus, and :disabled states.
  5. 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

Module 2: React Components

Module 3: Working with Events

Module 4: Advanced Component Concepts

Module 5: React Hooks

Module 6: Routing in React

Module 7: State Management

Module 8: Performance Optimization

Module 9: Testing React Applications

Module 10: Advanced Topics

Module 11: Project: Building a Complete Application

© Copyright 2026. All rights reserved