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
- El problema: el estilo también es parte del componente
- Estrategia 1: CSS global importado
- Estrategia 2: estilos en línea con objetos
- Estrategia 3: CSS Modules con Vite
- Estrategia 4: CSS-in-JS y frameworks de utilidades
- Clases condicionales según el estado de la bicicleta
- Variables CSS para la paleta de CicloUrbano
- Comparativa final y decisión del curso
- 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)"]
- 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/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 componentePero 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.
- 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, noborder-radius. - Valores como cadenas, con unidades incluidas. Los números sin unidad se interpretan como píxeles:
padding: 16equivale a16px.
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.
- 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. Comobicicleta.tipovale'urbana','electrica'o'carga', obtenemos la clase de variante correspondiente. Así se conectan los datos con el estilo sin ningúnif.- Clases con guiones: si nombras una clase
.tarjeta-destacada, en JS tendrás que escribirestilos['tarjeta-destacada']. Por eso en CSS Modules se estila usar camelCase:.tarjetaDestacada→estilos.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)}.
- 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 | Sí | No | Sí | Sí | Sí |
| 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 |
- 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:
...nombreses un parámetro rest: recoge todos los argumentos en un array..filter(Boolean)elimina los valores falsy del array:false,undefined,null,''y0. Es un atajo idiomático equivalente a.filter(valor => Boolean(valor))..join(' ')une los que quedan separados por espacios, que es justo el formato declassName.
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;
}
- 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.
- 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 |
Sí | No | Sí | Sí | Sí |
| Media queries | Sí | No | Sí | Sí | Sí |
| Animaciones | Sí | No | Sí | Sí | Sí |
| 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 | Sí | Sí | Sí | Sí |
| 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
classen lugar declassName. El error más repetido de React.classes palabra reservada de JavaScript. - Olvidar el
.moduleen el nombre del fichero.TarjetaBicicleta.csses global; soloTarjetaBicicleta.module.cssactiva el ámbito local. El síntoma es que elimport estilosllega vacío y las clases no se aplican. - Escribir
className={estilos}en lugar declassName={estilos.tarjeta}.estiloses un objeto; React acabará pintando[object Object]como clase. - Referirse a una clase que no existe en el módulo.
estilos.tarjeatvaleundefinedy 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-destacadaobliga aestilos['tarjeta-destacada']. Usa camelCase en los módulos. - Insertar
falseen una plantilla de clase.`tarjeta ${destacada && 'destacada'}`produce literalmente"tarjeta false"cuando la condición es falsa. Usa el helperclaseso un ternario con cadena vacía. - Poner unidades donde React ya las pone, o no ponerlas donde hacen falta. En
style,width: 300es300px, perowidth: '80%'necesita ser cadena. Yflex: 1ozIndex: 10van sin unidad. - Intentar
:hovercon 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.
.tituloy.precioenvejecen bien;.textoVerdeno. - 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
EtiquetaEstadoacierta 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.
- El reinicio de márgenes y el
box-sizingde toda la aplicación. - El color de fondo de una fila de tabla que depende del estado de la bicicleta (tres valores posibles).
- La posición exacta de un tooltip, calculada a partir de las coordenadas del ratón.
- Un botón de marca reutilizable con estados
:hover,:focusy:disabled. - 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
- ¿Qué es React?
- Configuración del Entorno de Desarrollo
- Hola Mundo en React
- JSX: Extensión de Sintaxis de JavaScript
- Cómo Renderiza React: Virtual DOM y Reconciliación
Módulo 2: Componentes de React
- Entendiendo los Componentes
- Componentes Funcionales vs de Clase
- Props: Pasando Datos a Componentes
- State: Gestión del Estado del Componente
- Estilos en los Componentes: CSS, Módulos y Utilidades
Módulo 3: Trabajando con Eventos
- Manejo de Eventos en React
- Renderizado Condicional
- Listas y Claves
- Formularios y Componentes Controlados
- Validación de Formularios y Componentes No Controlados
- Accesibilidad en Componentes Interactivos
Módulo 4: Conceptos Avanzados de Componentes
- Elevando el Estado
- Composición vs Herencia
- Métodos del Ciclo de Vida de React
- Hooks: Introducción y Uso Básico
- Límites de Error: Capturar Fallos en la Interfaz
Módulo 5: Hooks de React
- Hook useState
- Hook useEffect
- Hook useRef y Acceso al DOM
- Hook useContext
- Hook useReducer
- Hooks Personalizados
Módulo 6: Enrutamiento en React
- Introducción a React Router
- Configuración de React Router
- Rutas Anidadas
- Navegación Programática
- Rutas Protegidas y Control de Acceso
Módulo 7: Gestión del Estado
- Introducción a la Gestión del Estado
- API de Contexto
- Redux: Introducción y Configuración
- Redux: Acciones y Reductores
- Redux: Conectando a React
- Estado del Servidor: Peticiones, Caché y Sincronización
Módulo 8: Optimización del Rendimiento
- Técnicas de Optimización del Rendimiento en React
- Memorización con React.memo
- Hooks useMemo y useCallback
- División de Código y Carga Perezosa
- Medir el Rendimiento con React DevTools Profiler
Módulo 9: Pruebas en React
- Introducción a las Pruebas
- Pruebas Unitarias con Jest
- Pruebas de Componentes con React Testing Library
- Pruebas de Código Asíncrono y Simulación de APIs
- Pruebas de Extremo a Extremo con Cypress
Módulo 10: Temas Avanzados
- Renderizado del Lado del Servidor (SSR) con Next.js
- Generación de Sitios Estáticos (SSG) con Next.js
- Suspense y React Server Components
- TypeScript con React
- React Native: Creación de Aplicaciones Móviles
