En la primera lección del módulo dijimos que un componente encapsula tres cosas: marcado, lógica y estilo. Las dos primeras ya están resueltas: el marcado vive en el JSX y la lógica en el cuerpo de la función. El estilo, en cambio, sigue amontonado en un único src/index.css que crece sin control y donde nada impide que dentro de tres meses dos personas distintas definan una clase .tarjeta con reglas incompatibles. React no impone ninguna solución a esto: te ofrece varias estrategias y espera que elijas con criterio. En esta lección verás las cuatro que se usan en la práctica, todas aplicadas al mismo componente —TarjetaBicicleta— para poder compararlas de verdad, aprenderás a construir nombres de clase dinámicos a partir del estado de una bicicleta, ordenarás la paleta de CicloUrbano con variables CSS y adoptarás la estrategia que el proyecto usará de aquí en adelante.

Contenido

  1. El problema: el estilo también es parte del componente
  2. Estrategia 1: CSS global importado
  3. Estrategia 2: estilos en línea con objetos
  4. Estrategia 3: CSS Modules con Vite
  5. Estrategia 4: CSS-in-JS y frameworks de utilidades
  6. Clases condicionales según el estado de la bicicleta
  7. Variables CSS para la paleta de CicloUrbano
  8. Comparativa final y decisión del curso

  1. El problema: el estilo también es parte del componente

Hoy TarjetaBicicleta está incompleta como unidad: su marcado y su lógica están en TarjetaBicicleta.jsx, pero sus reglas de estilo están a cien líneas de distancia, en un fichero que comparte con todo lo demás. Eso produce cuatro problemas concretos:

Problema Cómo se manifiesta
Colisiones de nombres Todo el CSS vive en un espacio global. Una clase .tarjeta en dos sitios se pisa; gana la que se cargue después
Estilos huérfanos Al borrar un componente, nadie se acuerda de borrar su CSS. El fichero global solo crece
Miedo a modificar ¿Puedo cambiar .estado? Nadie sabe cuántos componentes la usan. Se acaba añadiendo .estado-nuevo en lugar de tocar la existente
Sin dinamismo El CSS no conoce los datos. Pintar el borde del color del tipo de bicicleta exige que el componente participe

React no resuelve esto por ti, pero sí te da el ingrediente que faltaba: el componente puede decidir sus clases en tiempo de ejecución, porque className es una expresión JavaScript como cualquier otra.

Las cuatro estrategias que existen se pueden ordenar por dónde vive el estilo:

flowchart TD
    A["¿Dónde vive el estilo?"] --> B["En un .css global<br/>(Estrategia 1)"]
    A --> C["En un objeto JS dentro del JSX<br/>(Estrategia 2)"]
    A --> D["En un .module.css junto al componente<br/>(Estrategia 3)"]
    A --> E["En el propio JSX como clases utilitarias<br/>o generado por JS (Estrategia 4)"]

  1. Estrategia 1: CSS global importado

Es la que has usado hasta ahora, y consiste en escribir CSS normal e importarlo.

/* src/index.css */
.tarjeta-bicicleta {
  background: #ffffff;
  border: 1px solid #d9e2ec;
  border-radius: 8px;
  padding: 1rem 1.25rem;
  margin-bottom: 1rem;
  max-width: 22rem;
}

.tarjeta-bicicleta h3 {
  margin-bottom: 0.5rem;
  color: #12805c;
}
// src/main.jsx  — se importa una vez y afecta a toda la aplicación
import './index.css';
// src/componentes/TarjetaBicicleta.jsx
function TarjetaBicicleta({ bicicleta }) {
  return (
    <article className="tarjeta-bicicleta">
      <h3>{bicicleta.modelo}</h3>
    </article>
  );
}

Vite también permite importar un .css desde el propio componente, lo cual mejora algo la organización:

// src/componentes/TarjetaBicicleta.jsx
import './TarjetaBicicleta.css';   // el fichero vive junto al componente

Pero cuidado: esto no lo hace local. Vite recoge todos los .css importados y los junta en la hoja de estilos de la página. El fichero está más cerca del componente, sí, pero sus clases siguen siendo globales.

El problema de las colisiones, con un ejemplo real

Imagina que dentro de dos meses alguien añade una pantalla de reservas con su propia hoja:

/* src/componentes/TarjetaReserva.css */
.tarjeta-bicicleta {          /* reutiliza el nombre "porque se parece" */
  max-width: 100%;
  border: none;
  background: #f5f7fa;
}

Ese fichero se importa y, sin que nadie lo pretenda, las tarjetas del catálogo pierden su borde y su ancho máximo. No hay error, no hay aviso: solo un diseño roto que aparece al navegar en cierto orden. Depurarlo lleva media tarde.

Las mitigaciones tradicionales existen y funcionan a medias:

  • Convenciones de nombres tipo BEM: .tarjeta-bicicleta__titulo, .tarjeta-bicicleta--urbana. Reducen mucho el riesgo, pero dependen de la disciplina de todo el equipo, para siempre.
  • Prefijos por componente: la misma idea, con la misma fragilidad.
Ventajas Inconvenientes
CSS puro, sin nada que aprender Espacio de nombres global: colisiones garantizadas a largo plazo
Funciona en cualquier proyecto El estilo no viaja con el componente
Ideal para reinicios, tipografía y variables Nadie se atreve a borrar reglas: el fichero solo crece

Cuándo usarlo: para lo que debe ser global. Reinicios (box-sizing), tipografía base, colores del body y, sobre todo, las variables CSS de la paleta, como verás en el apartado 7.

  1. Estrategia 2: estilos en línea con objetos

React permite pasar un objeto JavaScript al atributo style:

function TarjetaBicicleta({ bicicleta }) {
  return (
    <article
      style={{
        background: '#ffffff',
        border: '1px solid #d9e2ec',
        borderRadius: '8px',
        padding: '1rem 1.25rem',
        maxWidth: '22rem'
      }}
    >
      <h3 style={{ color: '#12805c', marginBottom: '0.5rem' }}>{bicicleta.modelo}</h3>
    </article>
  );
}

Recuerda de la lección 01-04 las reglas de este objeto:

  • Doble llave: las de fuera abren la expresión JSX, las de dentro son el literal de objeto.
  • Propiedades en camelCase: borderRadius, no border-radius.
  • Valores como cadenas, con unidades incluidas. Los números sin unidad se interpretan como píxeles: padding: 16 equivale a 16px.

Dónde brillan: valores calculados en tiempo de ejecución

Este es su caso legítimo, y es real. Una barra de ocupación de una estación cuyo ancho depende de los datos no se puede expresar con una clase CSS, porque el porcentaje solo se conoce al ejecutar:

// src/componentes/DisponibilidadEstacion.jsx

/**
 * Barra de ocupación de una estación.
 * Props:
 *  - estacion (objeto, obligatorio) { id, nombre, barrio, plazas }
 *  - ocupadas (número, opcional, por defecto 0)
 */
function DisponibilidadEstacion({ estacion, ocupadas = 0 }) {
  const porcentaje = Math.round((ocupadas / estacion.plazas) * 100);

  return (
    <div className="disponibilidad-estacion">
      <p>
        {estacion.nombre}: {ocupadas} de {estacion.plazas} plazas ocupadas
      </p>
      <div className="disponibilidad-estacion__barra">
        {/* El ancho solo se conoce en ejecución: caso legítimo de estilo en línea */}
        <div
          className="disponibilidad-estacion__relleno"
          style={{ width: `${porcentaje}%` }}
        />
      </div>
    </div>
  );
}

export default DisponibilidadEstacion;

Aquí la división del trabajo es la correcta: la clase aporta todo lo estático (colores, altura, bordes) y el estilo en línea aporta el único valor que depende de los datos.

Sus límites, que son severos

Limitación Consecuencia
Sin pseudoclases No hay :hover, :focus, :active, :disabled
Sin pseudoelementos No hay ::before ni ::after
Sin media queries No hay diseño adaptable
Sin selectores descendentes Hay que estilar cada nodo uno a uno
Sin animaciones No hay @keyframes
Especificidad máxima Un estilo en línea gana a casi todo; sobrescribirlo obliga a !important
Objeto nuevo en cada render El literal {{…}} crea un objeto distinto cada vez, con implicaciones para las optimizaciones del Módulo 8
Sin caché ni compresión El estilo viaja en el JavaScript, no en una hoja cacheable

Cuándo usarlo: solo para valores dinámicos que el CSS no puede conocer —anchos porcentuales, posiciones, transformaciones calculadas— y siempre combinado con una clase que aporte el resto. Nunca como estrategia principal.

  1. Estrategia 3: CSS Modules con Vite

Un CSS Module es un fichero CSS normal cuyo nombre acaba en .module.css. Vite lo detecta automáticamente —sin ninguna configuración— y hace dos cosas: renombra cada clase para que sea única en toda la aplicación y te devuelve un objeto que traduce tus nombres a los generados.

Cómo se escribe

/* src/componentes/TarjetaBicicleta.module.css */
.tarjeta {
  background: #ffffff;
  border: 1px solid #d9e2ec;
  border-radius: 8px;
  padding: 1rem 1.25rem;
  margin-bottom: 1rem;
  max-width: 22rem;
  border-left: 4px solid #d9e2ec;
}

.titulo {
  margin: 0 0 0.5rem;
  color: #12805c;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.dato {
  margin: 0.25rem 0;
  font-size: 0.95rem;
}

.precio {
  margin-top: 0.75rem;
  font-weight: 700;
}

/* Variantes por tipo de bicicleta */
.urbana { border-left-color: #12805c; }
.electrica { border-left-color: #b45309; }
.carga { border-left-color: #9b1c1c; }

/* Aquí SÍ funcionan pseudoclases, medias y todo lo demás */
.tarjeta:hover {
  box-shadow: 0 2px 8px rgba(31, 41, 51, 0.12);
}

@media (max-width: 30rem) {
  .tarjeta {
    max-width: 100%;
  }
}

Fíjate en los nombres: .tarjeta, .titulo, .dato. Cortos y genéricos, porque el ámbito local hace innecesario el prefijo. No hay ningún riesgo de que choquen con las .tarjeta de otro componente.

Cómo se usa

// src/componentes/TarjetaBicicleta.jsx
import estilos from './TarjetaBicicleta.module.css';
import EtiquetaEstado from './EtiquetaEstado.jsx';

function TarjetaBicicleta({ bicicleta, nombreEstacion = 'Estación desconocida' }) {
  const precioFormateado = bicicleta.precioHora.toFixed(2).replace('.', ',');

  return (
    <article className={`${estilos.tarjeta} ${estilos[bicicleta.tipo]}`}>
      <h3 className={estilos.titulo}>
        {bicicleta.modelo} <EtiquetaEstado estado={bicicleta.estado} />
      </h3>
      <p className={estilos.dato}>Tipo: {bicicleta.tipo}</p>
      <p className={estilos.dato}>Estación: {nombreEstacion}</p>
      <p className={estilos.precio}>{precioFormateado} € / hora</p>
    </article>
  );
}

export default TarjetaBicicleta;

Qué está pasando por debajo

import estilos from './TarjetaBicicleta.module.css' no importa texto: importa un objeto que Vite genera al procesar el fichero.

// Contenido aproximado del objeto 'estilos' en desarrollo
{
  tarjeta: '_tarjeta_1x9k2_1',
  titulo: '_titulo_1x9k2_11',
  dato: '_dato_1x9k2_18',
  precio: '_precio_1x9k2_23',
  urbana: '_urbana_1x9k2_28',
  electrica: '_electrica_1x9k2_29',
  carga: '_carga_1x9k2_30'
}

Y el HTML que llega al navegador:

<article class="_tarjeta_1x9k2_1 _urbana_1x9k2_28">
  <h3 class="_titulo_1x9k2_11">Urbana Clásica <span class="estado estado--disponible">disponible</span></h3>
  …
</article>

Ese sufijo se calcula a partir del nombre del fichero y de su contenido, de modo que es imposible que dos componentes distintos generen la misma clase. El problema de las colisiones desaparece por construcción, no por disciplina.

Dos detalles de sintaxis útiles:

  • estilos[bicicleta.tipo]: acceso por corchetes. Como bicicleta.tipo vale 'urbana', 'electrica' o 'carga', obtenemos la clase de variante correspondiente. Así se conectan los datos con el estilo sin ningún if.
  • Clases con guiones: si nombras una clase .tarjeta-destacada, en JS tendrás que escribir estilos['tarjeta-destacada']. Por eso en CSS Modules se estila usar camelCase: .tarjetaDestacadaestilos.tarjetaDestacada.

Ventajas e inconvenientes

Ventajas Inconvenientes
Ámbito local garantizado por la herramienta Un fichero extra por componente
Es CSS de verdad: pseudoclases, medias, animaciones, todo Los nombres generados son ilegibles al inspeccionar (configurable)
El estilo viaja con el componente: se borra junto a él Los valores dinámicos siguen necesitando estilos en línea o variables CSS
Sin dependencias: Vite lo trae de serie Hay que recordar estilos. delante de cada clase
El navegador puede cachear la hoja resultante Compartir estilos entre componentes requiere pensarlo (composes o un módulo común)

Esta es la estrategia por defecto de CicloUrbano a partir de ahora, por tres razones: no añade ninguna dependencia, resuelve el problema real (las colisiones) sin pedirte disciplina, y sigue siendo CSS, de modo que todo lo que sabes de la cascada te sirve igual.

Estilos compartidos entre módulos

Cuando dos componentes necesitan las mismas reglas, hay dos vías limpias:

/* src/estilos/comunes.module.css */
.caja {
  background: #ffffff;
  border: 1px solid #d9e2ec;
  border-radius: 8px;
  padding: 1rem 1.25rem;
}
/* src/componentes/TarjetaBicicleta.module.css */
.tarjeta {
  composes: caja from '../estilos/comunes.module.css';
  margin-bottom: 1rem;
  max-width: 22rem;
}

composes es una funcionalidad propia de CSS Modules: la clase resultante incluye las dos. La alternativa, más sencilla, es poner esas reglas comunes en el CSS global y combinarlas con el helper del apartado 6: className={clases('caja', estilos.tarjeta)}.

  1. Estrategia 4: CSS-in-JS y frameworks de utilidades

Las dos alternativas restantes son muy populares y conviene conocerlas aunque no las usemos en el curso.

CSS-in-JS

Librerías como styled-components o Emotion permiten escribir CSS dentro de JavaScript y obtener un componente ya estilado:

// Ejemplo con styled-components (NO se usa en este curso)
import styled from 'styled-components';

const Tarjeta = styled.article`
  background: #ffffff;
  border: 1px solid #d9e2ec;
  border-radius: 8px;
  padding: 1rem 1.25rem;

  /* Las props llegan a la plantilla: dinamismo total */
  border-left: 4px solid ${props =>
    props.$tipo === 'electrica' ? '#b45309' : '#12805c'};

  &:hover {
    box-shadow: 0 2px 8px rgba(31, 41, 51, 0.12);
  }
`;

<Tarjeta $tipo={bicicleta.tipo}>…</Tarjeta>

Su gran atractivo es que las props determinan el estilo de forma directa, con toda la potencia de CSS. Sus inconvenientes: una dependencia más, un coste en tiempo de ejecución (los estilos se generan mientras la aplicación corre), fricción con el renderizado en servidor y una tendencia clara del ecosistema a alejarse de este enfoque a favor de soluciones que resuelven en tiempo de compilación (zero-runtime), como Vanilla Extract o Panda CSS.

Frameworks de utilidades (Tailwind CSS)

El enfoque opuesto: en lugar de escribir CSS, se componen clases atómicas predefinidas directamente en el JSX.

// Ejemplo con Tailwind CSS (NO se usa en este curso)
<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">{bicicleta.modelo}</h3>
</article>

Ventajas: no se cambia de fichero para estilar, el CSS final es diminuto porque solo se incluyen las utilidades usadas, y el sistema de diseño (espaciados, colores, tipografías) viene impuesto, lo que da mucha consistencia. Inconvenientes: el JSX se llena de clases y pierde legibilidad, hay una curva de aprendizaje de nombres, y componer condicionales largos exige una librería auxiliar.

Las cuatro, en perspectiva

Global En línea CSS Modules CSS-in-JS Utilidades
Ámbito Global Del elemento Local Local Global (atómico)
Pseudoclases y medias No
Dinamismo por props No Total Por clases o variables Total Por clases
Dependencias Ninguna Ninguna Ninguna (Vite) Una librería Una herramienta
Coste en ejecución Nulo Bajo Nulo Medio Nulo

  1. Clases condicionales según el estado de la bicicleta

Cualquiera que sea la estrategia, hay una necesidad constante: elegir clases en función de los datos. En CicloUrbano, el distintivo de estado debe ser verde si la bicicleta está disponible, naranja si está alquilada y rojo si está en mantenimiento.

Con plantillas de cadena

La forma más directa, y la que ya usaste en EtiquetaEstado:

// src/componentes/EtiquetaEstado.jsx
function EtiquetaEstado({ estado = 'disponible' }) {
  return <span className={`estado estado--${estado}`}>{estado}</span>;
}

estado--${estado} produce estado--disponible, estado--alquilada o estado--mantenimiento. Una sola línea cubre los tres casos y añadir un cuarto estado no exige tocar el componente, solo el CSS.

Cuidado con un límite importante: esto funciona con CSS global, pero no con CSS Modules, porque las clases de un módulo están renombradas. Ahí hay que acceder al objeto:

import estilos from './EtiquetaEstado.module.css';

function EtiquetaEstado({ estado = 'disponible' }) {
  return <span className={`${estilos.etiqueta} ${estilos[estado]}`}>{estado}</span>;
}

Con condicionales

Cuando la clase depende de un booleano:

// Ternario: una condición
<article className={destacada ? 'tarjeta-bicicleta destacada' : 'tarjeta-bicicleta'}>

// Con && dentro de la plantilla: cuidado, si es false inserta "false"
<article className={`tarjeta-bicicleta ${destacada ? 'destacada' : ''}`}>

Esto se vuelve ilegible enseguida. Con tres o cuatro condiciones aparecen plantillas monstruosas, espacios dobles y undefined colándose en el atributo.

Un pequeño helper

La solución es una función que junte los nombres válidos y descarte lo demás. Ocupa tres líneas y la usarás en todos tus proyectos:

// src/utilidades/clases.js

/**
 * Une nombres de clase descartando los valores falsy.
 * clases('tarjeta', false, undefined, 'activa')  ->  'tarjeta activa'
 */
export function clases(...nombres) {
  return nombres.filter(Boolean).join(' ');
}

Cómo funciona, paso a paso:

  • ...nombres es un parámetro rest: recoge todos los argumentos en un array.
  • .filter(Boolean) elimina los valores falsy del array: false, undefined, null, '' y 0. Es un atajo idiomático equivalente a .filter(valor => Boolean(valor)).
  • .join(' ') une los que quedan separados por espacios, que es justo el formato de className.

Con él, el JSX vuelve a ser legible:

// src/componentes/TarjetaBicicleta.jsx
import { clases } from '../utilidades/clases.js';
import estilos from './TarjetaBicicleta.module.css';
import EtiquetaEstado from './EtiquetaEstado.jsx';

function TarjetaBicicleta({ bicicleta, nombreEstacion = 'Estación desconocida', destacada = false }) {
  const precioFormateado = bicicleta.precioHora.toFixed(2).replace('.', ',');
  const noDisponible = bicicleta.estado !== 'disponible';

  return (
    <article
      className={clases(
        estilos.tarjeta,
        estilos[bicicleta.tipo],
        destacada && estilos.destacada,
        noDisponible && estilos.atenuada
      )}
    >
      <h3 className={estilos.titulo}>
        {bicicleta.modelo} <EtiquetaEstado estado={bicicleta.estado} />
      </h3>
      <p className={estilos.dato}>Tipo: {bicicleta.tipo}</p>
      <p className={estilos.dato}>Estación: {nombreEstacion}</p>
      <p className={estilos.precio}>{precioFormateado} € / hora</p>
    </article>
  );
}

export default TarjetaBicicleta;

Si destacada es false, la expresión destacada && estilos.destacada vale false, y filter(Boolean) la descarta. Sin ternarios, sin cadenas vacías, sin espacios sobrantes.

En proyectos reales verás la librería clsx (o classnames), que hace esto mismo con más funcionalidades. Nuestro helper de tres líneas cubre el caso habitual sin añadir dependencias.

Y las clases que faltan en el módulo:

/* Añadir a src/componentes/TarjetaBicicleta.module.css */
.destacada {
  box-shadow: 0 0 0 2px #12805c;
}

.atenuada {
  opacity: 0.7;
}

  1. Variables CSS para la paleta de CicloUrbano

Los colores de CicloUrbano llevan repitiéndose literalmente desde la lección 01-03: #12805c en cinco sitios, #d9e2ec en otros tantos. Cambiar el verde de marca implicaría hoy una búsqueda y sustitución por todo el proyecto, con el riesgo de dejarse alguno.

Las variables CSS (propiedades personalizadas) resuelven esto de forma nativa, sin herramientas ni preprocesadores:

/* src/index.css — al principio del fichero */
:root {
  /* Paleta de marca */
  --color-marca: #12805c;
  --color-alquilada: #b45309;
  --color-mantenimiento: #9b1c1c;

  /* Superficies y texto */
  --color-fondo: #f5f7fa;
  --color-superficie: #ffffff;
  --color-texto: #1f2933;
  --color-borde: #d9e2ec;

  /* Formas y espaciado */
  --radio: 8px;
  --sombra: 0 2px 8px rgba(31, 41, 51, 0.12);
  --espacio: 1rem;
}

Y ahora cualquier fichero de estilo del proyecto puede usarlas, incluidos los .module.css, porque las variables sí atraviesan el ámbito local: lo que CSS Modules renombra son las clases, no las propiedades personalizadas.

/* src/componentes/TarjetaBicicleta.module.css */
.tarjeta {
  background: var(--color-superficie);
  border: 1px solid var(--color-borde);
  border-radius: var(--radio);
  padding: var(--espacio) 1.25rem;
  border-left: 4px solid var(--color-borde);
}

.tarjeta:hover {
  box-shadow: var(--sombra);
}

.titulo {
  color: var(--color-marca);
}

.urbana { border-left-color: var(--color-marca); }
.electrica { border-left-color: var(--color-alquilada); }
.carga { border-left-color: var(--color-mantenimiento); }

Ventajas concretas:

Ventaja Ejemplo
Una sola fuente de verdad Cambiar --color-marca repinta toda la aplicación
Se pueden cambiar en ejecución Un tema oscuro es cuestión de redefinir las variables bajo otro selector
Funcionan con CSS Modules El ámbito local no afecta a las propiedades personalizadas
Puente perfecto con JS Un estilo en línea puede fijar una variable, y el CSS la consume

Ese último punto merece un ejemplo, porque combina lo mejor de las estrategias 2 y 3:

// El componente calcula el valor y lo entrega como variable CSS
<div
  className={estilos.relleno}
  style={{ '--ancho-ocupacion': `${porcentaje}%` }}
/>
.relleno {
  width: var(--ancho-ocupacion, 0%);
  height: 100%;
  background: var(--color-marca);
  transition: width 0.3s ease;   /* la transición SÍ es posible: es CSS de verdad */
}

El JavaScript solo aporta el dato; toda la presentación —incluida la transición, imposible en un estilo en línea— sigue en el CSS. Es el patrón recomendado para estilos dinámicos.

  1. Comparativa final y decisión del curso

Criterio CSS global Estilos en línea CSS Modules CSS-in-JS Utilidades (Tailwind)
Ámbito Global Solo ese elemento Local automático Local automático Global atómico
Riesgo de colisión Alto Nulo Nulo Nulo Nulo
Pseudoclases y :hover No
Media queries No
Animaciones No
Dinamismo con datos Solo por clases Total Clases + variables CSS Total Clases condicionales
Herramientas necesarias Ninguna Ninguna Ninguna con Vite Una librería Una herramienta de compilación
Coste en ejecución Nulo Bajo Nulo Medio Nulo
El estilo viaja con el componente No
Curva de aprendizaje Nula Baja Baja Media Media
Cuándo usarlo Reinicios, tipografía, variables Valores calculados en ejecución Estilo de componentes: por defecto Sistemas de diseño con mucho dinamismo Equipos que quieren consistencia y velocidad

La combinación que adopta CicloUrbano

No es «elegir una y descartar el resto»: las estrategias se complementan, y este reparto es el que verás en la mayoría de proyectos con Vite:

flowchart TD
    A["src/index.css<br/>GLOBAL"] --> A1["Reinicio, tipografía,<br/>variables de la paleta"]
    B["Componente.module.css<br/>CSS MODULES"] --> B1["Todo el estilo del componente:<br/>layout, colores, hover, medias"]
    C["style={{ }}<br/>EN LÍNEA"] --> C1["Solo valores calculados,<br/>preferiblemente como variable CSS"]

Estructura de ficheros resultante:

src/
├── componentes/
│   ├── TarjetaBicicleta.jsx
│   ├── TarjetaBicicleta.module.css      <- el estilo vive junto al componente
│   ├── EtiquetaEstado.jsx
│   ├── EtiquetaEstado.module.css
│   └── …
├── estilos/
│   └── comunes.module.css               <- reglas compartidas entre módulos
├── utilidades/
│   └── clases.js                        <- el helper de clases condicionales
├── datos/
│   └── dominio.js
├── App.jsx
├── index.css                            <- reinicio + variables globales
└── main.jsx

Con esto, TarjetaBicicleta es por fin una unidad completa: su marcado, su lógica y su estilo están en la misma carpeta, con el mismo nombre, y se pueden borrar juntos sin dejar rastro. Esa era la promesa de la primera lección del módulo.

Errores Comunes y Consejos

  • Usar class en lugar de className. El error más repetido de React. class es palabra reservada de JavaScript.
  • Olvidar el .module en el nombre del fichero. TarjetaBicicleta.css es global; solo TarjetaBicicleta.module.css activa el ámbito local. El síntoma es que el import estilos llega vacío y las clases no se aplican.
  • Escribir className={estilos} en lugar de className={estilos.tarjeta}. estilos es un objeto; React acabará pintando [object Object] como clase.
  • Referirse a una clase que no existe en el módulo. estilos.tarjeat vale undefined y el elemento se queda sin clase, sin ningún error. Es el fallo más difícil de ver: revisa siempre el HTML generado en el inspector.
  • Escribir CSS con guiones en un módulo y acceder con punto. .tarjeta-destacada obliga a estilos['tarjeta-destacada']. Usa camelCase en los módulos.
  • Insertar false en una plantilla de clase. `tarjeta ${destacada && 'destacada'}` produce literalmente "tarjeta false" cuando la condición es falsa. Usa el helper clases o un ternario con cadena vacía.
  • Poner unidades donde React ya las pone, o no ponerlas donde hacen falta. En style, width: 300 es 300px, pero width: '80%' necesita ser cadena. Y flex: 1 o zIndex: 10 van sin unidad.
  • Intentar :hover con estilos en línea. No existe. Si lo necesitas, hace falta una clase.
  • Consejo: empieza por las variables CSS. Antes de escribir el primer componente de un proyecto, define la paleta en :root. Ahorra una refactorización garantizada.
  • Consejo: nombra las clases de un módulo por su papel, no por su aspecto. .titulo y .precio envejecen bien; .textoVerde no.
  • Consejo: si un componente necesita más de tres o cuatro clases condicionales, revisa su diseño. Suele ser señal de que hacen falta dos componentes distintos.
  • Nota sobre accesibilidad: el color nunca debe ser el único portador de información. Nuestro EtiquetaEstado acierta al mostrar también el texto del estado junto al color. Esto se trata a fondo en Accesibilidad en Componentes Interactivos.

Ejercicios

Ejercicio 1

Migra EtiquetaEstado de CSS global a CSS Modules. Crea src/componentes/EtiquetaEstado.module.css con una clase base y una clase por cada estado (disponible, alquilada, mantenimiento), usando las variables CSS de la paleta. Adapta el componente para que combine la clase base con la del estado recibido por props, y añade un efecto de :hover que aumente ligeramente la opacidad.

Pista: la clase del estado hay que obtenerla del objeto de estilos, no construirla con una plantilla de cadena.

Ejercicio 2

Crea el componente DisponibilidadEstacion completo (src/componentes/DisponibilidadEstacion.jsx y su módulo CSS). Debe recibir una estacion del dominio y un número de plazas ocupadas, y mostrar el nombre, el recuento y una barra de progreso. El ancho de la barra debe pasarse como variable CSS desde el componente, y el color debe cambiar según la ocupación: verde por debajo del 60 %, naranja entre el 60 % y el 85 %, rojo por encima.

Usa el helper clases para elegir la clase de color.

Ejercicio 3

Para cada situación, indica qué estrategia de estilo elegirías y por qué. Justifica en dos o tres frases.

  1. El reinicio de márgenes y el box-sizing de toda la aplicación.
  2. El color de fondo de una fila de tabla que depende del estado de la bicicleta (tres valores posibles).
  3. La posición exacta de un tooltip, calculada a partir de las coordenadas del ratón.
  4. Un botón de marca reutilizable con estados :hover, :focus y :disabled.
  5. La paleta de colores corporativa de CicloUrbano.

Soluciones

Solución 1.

/* src/componentes/EtiquetaEstado.module.css */
.etiqueta {
  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-superficie);
  opacity: 0.92;
  transition: opacity 0.2s ease;
}

.etiqueta:hover {
  opacity: 1;
}

.disponible { background-color: var(--color-marca); }
.alquilada { background-color: var(--color-alquilada); }
.mantenimiento { background-color: var(--color-mantenimiento); }
// src/componentes/EtiquetaEstado.jsx
import { clases } from '../utilidades/clases.js';
import estilos from './EtiquetaEstado.module.css';

/**
 * Distintivo visual del estado de una bicicleta.
 * Props:
 *  - estado (cadena, opcional, por defecto 'disponible'):
 *    'disponible' | 'alquilada' | 'mantenimiento'
 */
function EtiquetaEstado({ estado = 'disponible' }) {
  return (
    <span className={clases(estilos.etiqueta, estilos[estado])}>{estado}</span>
  );
}

export default EtiquetaEstado;

El punto clave: className={`estado estado--${estado}`} deja de funcionar con módulos, porque las clases están renombradas. Hay que consultarlas en el objeto con estilos[estado]. Y si llegara un estado inesperado, estilos[estado] sería undefined y el helper clases lo descartaría sin romper nada: la etiqueta se pintaría sin color de fondo en lugar de fallar.

Solución 2.

/* src/componentes/DisponibilidadEstacion.module.css */
.contenedor {
  background: var(--color-superficie);
  border: 1px solid var(--color-borde);
  border-radius: var(--radio);
  padding: var(--espacio) 1.25rem;
  margin-bottom: var(--espacio);
  max-width: 32rem;
}

.texto {
  margin: 0 0 0.5rem;
  font-size: 0.95rem;
}

.barra {
  height: 10px;
  background: var(--color-fondo);
  border: 1px solid var(--color-borde);
  border-radius: 999px;
  overflow: hidden;
}

.relleno {
  /* El ancho llega como variable CSS desde el componente */
  width: var(--ocupacion, 0%);
  height: 100%;
  transition: width 0.3s ease;
}

.baja { background: var(--color-marca); }
.media { background: var(--color-alquilada); }
.alta { background: var(--color-mantenimiento); }
// src/componentes/DisponibilidadEstacion.jsx
import { clases } from '../utilidades/clases.js';
import estilos from './DisponibilidadEstacion.module.css';

/**
 * Ocupación de una estación de CicloUrbano.
 * Props:
 *  - estacion (objeto, obligatorio) { id, nombre, barrio, plazas }
 *  - ocupadas (número, opcional, por defecto 0)
 */
function DisponibilidadEstacion({ estacion, ocupadas = 0 }) {
  const porcentaje = Math.round((ocupadas / estacion.plazas) * 100);
  const nivel = porcentaje < 60 ? 'baja' : porcentaje <= 85 ? 'media' : 'alta';

  return (
    <div className={estilos.contenedor}>
      <p className={estilos.texto}>
        {estacion.nombre} ({estacion.barrio}): {ocupadas} de {estacion.plazas} plazas
        ocupadas · {porcentaje} %
      </p>
      <div className={estilos.barra}>
        <div
          className={clases(estilos.relleno, estilos[nivel])}
          style={{ '--ocupacion': `${porcentaje}%` }}
        />
      </div>
    </div>
  );
}

export default DisponibilidadEstacion;
// src/App.jsx (fragmento)
<DisponibilidadEstacion estacion={estaciones[0]} ocupadas={11} />
<DisponibilidadEstacion estacion={estaciones[1]} ocupadas={13} />
<DisponibilidadEstacion estacion={estaciones[2]} ocupadas={9} />

Reparto de responsabilidades: el componente calcula solo el dato (porcentaje y nivel), el módulo CSS aporta toda la presentación —incluida la transición— y el estilo en línea se limita a entregar una variable. Tanto porcentaje como nivel son valores derivados: ninguno es estado, como estudiaste en la lección anterior.

Solución 3.

Caso Estrategia Justificación
1. Reinicio y box-sizing CSS global Es global por naturaleza: afecta a todos los elementos del documento. Encapsularlo por componente no tendría sentido
2. Fondo de fila según el estado CSS Modules con clase condicional Son tres valores conocidos de antemano: tres clases y un estilos[estado]. No hace falta dinamismo en ejecución
3. Posición de un tooltip Estilo en línea (o variable CSS) Las coordenadas solo se conocen en ejecución y son un continuo, no un conjunto de casos. Es el caso legítimo del estilo en línea
4. Botón de marca con :hover, :focus y :disabled CSS Modules Las pseudoclases son imposibles en línea, y el ámbito local evita que un .boton de otro componente lo pise
5. Paleta corporativa Variables CSS en :root (global) Fuente de verdad única, accesible desde cualquier módulo y modificable en ejecución para temas alternativos

Conclusión

Ya sabes dar estilo a un componente de React con criterio, no por costumbre. Has visto las cuatro estrategias sobre el mismo TarjetaBicicleta: el CSS global, cómodo pero con un espacio de nombres compartido donde las colisiones son cuestión de tiempo; los estilos en línea, imbatibles para valores calculados en ejecución e inútiles para todo lo demás —sin :hover, sin media queries, sin animaciones—; los CSS Modules, que Vite soporta de serie y que resuelven el ámbito local por construcción; y las alternativas del ecosistema, CSS-in-JS y frameworks de utilidades, con sus compromisos bien identificados.

CicloUrbano adopta la combinación que mejor equilibrio ofrece: variables CSS en :root para la paleta y las formas, un .module.css junto a cada componente para todo su estilo, y estilos en línea solo para entregar valores calculados, preferiblemente en forma de variable CSS para que la presentación siga viviendo en el CSS. Por el camino has incorporado dos herramientas que usarás constantemente: construir clases a partir de los datos con estilos[bicicleta.tipo] y componer clases condicionales con el helper clases, que descarta los valores falsos sin ensuciar el JSX.

Con esto cierras el Módulo 2. Sabes qué es un componente como unidad de diseño y cómo trazar sus límites; sabes leer el código heredado escrito con clases y por qué todo lo nuevo se escribe con funciones; sabes parametrizar un componente con props de solo lectura y componer contenido con children; sabes darle memoria con estado, actualizarlo de forma inmutable y distinguir lo que se guarda de lo que se deriva; y ahora sabes vestirlo sin que el estilo se descontrole. TarjetaBicicleta es, por fin, una unidad completa: marcado, lógica y estilo en la misma carpeta.

Pero el catálogo sigue siendo un escaparate. El SelectorTipo guarda el tipo elegido y no filtra nada; ContadorPlazas resta plazas que no le importan a nadie más; no hay formularios, no hay validación y las listas se escriben tarjeta a tarjeta. Falta la interacción de verdad. En el Módulo 3: Trabajando con Eventos aprenderás a manejar los eventos de React con todas sus reglas, a mostrar u ocultar partes de la interfaz con renderizado condicional, a pintar colecciones completas con map y a entender por fin para qué sirven las claves que viste en la reconciliación, a construir formularios controlados y a validarlos, y a hacer que todo ello sea accesible. La próxima lección es Manejo de Eventos en React, y a partir de ahí CicloUrbano dejará de mirarse y empezará a responder.

Curso de React

Módulo 1: Introducción a React

Módulo 2: Componentes de React

Módulo 3: Trabajando con Eventos

Módulo 4: Conceptos Avanzados de Componentes

Módulo 5: Hooks de React

Módulo 6: Enrutamiento en React

Módulo 7: Gestión del Estado

Módulo 8: Optimización del Rendimiento

Módulo 9: Pruebas en React

Módulo 10: Temas Avanzados

Módulo 11: Proyecto: Construyendo una Aplicación Completa

© Copyright 2026. Todos los derechos reservados