Tema
Estilos
Hay muchas formas de dar estilo en React. En esta lección verás las dos más usadas en proyectos profesionales: CSS Modules (CSS normal, pero con clases que no chocan entre componentes) y Tailwind CSS v4 (clases utilitarias directamente en el JSX). También cómo manejar estilos dinámicos y modo oscuro.
Concepto
El problema del CSS global: si dos componentes usan la clase .titulo, sus estilos se mezclan. En un proyecto con cientos de componentes, esto se vuelve un dolor de cabeza.
| Enfoque | Cómo funciona | Pros | Contras |
|---|---|---|---|
| CSS global | import './App.css' | Simple | Choques de nombres |
| CSS Modules | import s from './X.module.css' | CSS normal, clases aisladas | Archivo extra por componente |
| Tailwind | Clases utilitarias en className | Muy rápido, consistente, sin CSS muerto | JSX con muchas clases |
| Estilos en línea | style con un objeto | Valores dinámicos | Sin pseudo-clases ni media queries |
| CSS-in-JS (styled-components) | CSS dentro de JS | Dinámico | Costo en rendimiento, menos usado hoy |
CSS Modules
Vite los soporta sin configuración: cualquier archivo terminado en .module.css.
css
.boton {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 1.2rem;
border: 0;
border-radius: 8px;
font: inherit;
font-weight: 600;
cursor: pointer;
transition: transform 0.1s, background-color 0.2s;
}
.boton:active { transform: scale(0.97); }
.boton:disabled { opacity: 0.5; cursor: not-allowed; }
.primario { background: #6d28d9; color: white; }
.primario:hover { background: #5b21b6; }
.secundario { background: #ede9fe; color: #5b21b6; }
.peligro { background: #dc2626; color: white; }
.grande { padding: 0.9rem 1.6rem; font-size: 1.1rem; }jsx
import styles from './Boton.module.css'
export default function Boton({ variante = 'primario', grande = false, children, ...props }) {
// styles.boton se convierte en algo como "_boton_x7k2a_1": único en toda la app
const clases = [styles.boton, styles[variante], grande && styles.grande].filter(Boolean).join(' ')
return (
<button className={clases} {...props}>
{children}
</button>
)
}jsx
import Boton from './components/Boton'
export default function App() {
return (
<div style={{ display: 'flex', gap: 12, padding: 24 }}>
<Boton>Guardar</Boton>
<Boton variante="secundario">Cancelar</Boton>
<Boton variante="peligro" grande>Eliminar</Boton>
<Boton disabled>Deshabilitado</Boton>
</div>
)
}Para combinar clases con comodidad, la librería clsx es muy popular:
bash
npm i clsxjsx
import clsx from 'clsx'
const clases = clsx(styles.boton, styles[variante], { [styles.grande]: grande })Variables CSS para temas
Define colores y espaciados como variables en el CSS global y úsalas en tus módulos:
css
/* src/index.css */
:root {
--color-primario: #6d28d9;
--color-fondo: #ffffff;
--color-texto: #1f2937;
--radio: 8px;
}
[data-tema='oscuro'] {
--color-fondo: #0f172a;
--color-texto: #e2e8f0;
}
body {
background: var(--color-fondo);
color: var(--color-texto);
}css
/* Tarjeta.module.css */
.tarjeta {
border-radius: var(--radio);
border: 1px solid color-mix(in srgb, var(--color-texto) 15%, transparent);
}Tailwind CSS v4
Tailwind te da miles de clases pequeñas (p-4, text-lg, bg-violet-600, rounded-xl) que combinas directamente en el JSX. Su versión 4 se configura en segundos.
Instalación con Vite
bash
npm i tailwindcss @tailwindcss/vitejs
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react(), tailwindcss()]
})css
@import "tailwindcss";
/* Personaliza tu diseño con variables (Tailwind v4 usa CSS, no tailwind.config.js) */
@theme {
--color-marca-50: #f5f3ff;
--color-marca-500: #8b5cf6;
--color-marca-600: #7c3aed;
--color-marca-700: #6d28d9;
--font-display: "Inter", system-ui, sans-serif;
}Con eso ya puedes usar bg-marca-600, text-marca-700 y font-display.
Ejemplo: tarjeta de producto
jsx
export default function TarjetaProducto({ producto, onAgregar }) {
const { titulo, precio, imagen, descuento } = producto
return (
<article className="group overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm transition hover:-translate-y-1 hover:shadow-lg dark:border-gray-700 dark:bg-gray-900">
<div className="relative aspect-square overflow-hidden bg-gray-100">
<img
src={imagen}
alt={titulo}
className="h-full w-full object-cover transition duration-300 group-hover:scale-105"
/>
{descuento > 0 && (
<span className="absolute left-3 top-3 rounded-full bg-red-600 px-2 py-1 text-xs font-bold text-white">
-{descuento}%
</span>
)}
</div>
<div className="space-y-2 p-4">
<h3 className="line-clamp-2 font-semibold text-gray-900 dark:text-white">{titulo}</h3>
<p className="text-xl font-bold text-marca-700 dark:text-marca-500">${precio}</p>
<button
onClick={onAgregar}
className="w-full rounded-lg bg-marca-600 py-2 font-medium text-white hover:bg-marca-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-marca-600 active:scale-95"
>
Agregar al carrito
</button>
</div>
</article>
)
}Lo esencial de Tailwind
| Necesidad | Clases |
|---|---|
| Espaciado | p-4, px-6, mt-2, gap-4, space-y-2 |
| Tamaño | w-full, h-12, max-w-md, min-h-screen |
| Flex / Grid | flex items-center justify-between, grid grid-cols-3 |
| Texto | text-sm, font-bold, text-gray-600, text-center |
| Fondo y borde | bg-white, border, rounded-xl, shadow-md |
| Estados | hover:, focus-visible:, active:, disabled: |
| Responsive (mobile first) | sm:, md:, lg:, xl: |
| Modo oscuro | dark: |
Responsive: mobile first
Las clases sin prefijo aplican a todas las pantallas; los prefijos aplican desde ese tamaño hacia arriba:
jsx
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{productos.map((p) => <TarjetaProducto key={p.id} producto={p} />)}
</div>Modo oscuro con una clase
Por defecto dark: sigue la preferencia del sistema. Para controlarlo con un botón, define una variante personalizada:
css
/* src/index.css */
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));jsx
function BotonTema() {
const alternar = () => document.documentElement.classList.toggle('dark')
return (
<button onClick={alternar} className="rounded-lg border px-3 py-1 dark:border-gray-600">
🌓 Tema
</button>
)
}Clases condicionales sin conflictos
Cuando combinas clases de Tailwind según props, usa clsx + tailwind-merge para que la última gane si hay conflicto (p-2 vs p-4):
bash
npm i clsx tailwind-mergejs
// src/lib/cn.js
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...entradas) {
return twMerge(clsx(entradas))
}jsx
import { cn } from '../lib/cn'
const VARIANTES = {
primario: 'bg-marca-600 text-white hover:bg-marca-700',
secundario: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
fantasma: 'bg-transparent hover:bg-gray-100'
}
export function Boton({ variante = 'primario', className, ...props }) {
return (
<button
className={cn(
'inline-flex items-center gap-2 rounded-lg px-4 py-2 font-medium transition disabled:opacity-50',
VARIANTES[variante],
className // permite sobrescribir desde fuera: <Boton className="w-full" />
)}
{...props}
/>
)
}Este patrón es la base de shadcn/ui, una colección de componentes accesibles que copias a tu proyecto y personalizas con Tailwind. Muy usada en proyectos profesionales.
Estilos dinámicos
Para valores calculados (un porcentaje, un color elegido), combina clases con style o variables CSS:
jsx
function BarraProgreso({ porcentaje, color = '#7c3aed' }) {
return (
<div className="h-3 w-full overflow-hidden rounded-full bg-gray-200">
<div
className="h-full rounded-full transition-all duration-500"
style={{ width: `${porcentaje}%`, backgroundColor: color }}
role="progressbar"
aria-valuenow={porcentaje}
aria-valuemin={0}
aria-valuemax={100}
/>
</div>
)
}Error común con Tailwind
Construir nombres de clase con variables: `bg-${color}-500`. Tailwind analiza tu código buscando clases completas; las generadas dinámicamente no existen en el CSS final. Usa un objeto de mapeo con clases completas (como VARIANTES arriba).
¿Cuál elijo?
- Tailwind: la opción más popular hoy para proyectos nuevos; ideal si trabajas rápido y con un sistema de diseño consistente.
- CSS Modules: si prefieres CSS tradicional o el equipo ya domina CSS.
- Ambos pueden convivir en un mismo proyecto. Repasa los fundamentos en el curso de CSS.
Ejercicios
- Con CSS Modules, crea un componente
Alertacon variantesinfo,exito,advertenciayerror, con ícono y botón para cerrar. - Con Tailwind, crea un
Navbarresponsive: en móvil muestra un botón de hamburguesa que despliega el menú; desdemdmuestra los enlaces en línea.
Ver solución
css
.alerta { display: flex; gap: 12px; align-items: flex-start; padding: 12px 16px; border-radius: 8px; border-left: 4px solid; }
.info { background: #eff6ff; border-color: #3b82f6; color: #1e3a8a; }
.exito { background: #f0fdf4; border-color: #22c55e; color: #14532d; }
.advertencia { background: #fffbeb; border-color: #f59e0b; color: #78350f; }
.error { background: #fef2f2; border-color: #ef4444; color: #7f1d1d; }
.cerrar { margin-left: auto; background: none; border: 0; cursor: pointer; font-size: 1.1rem; color: inherit; }jsx
import { useState } from 'react'
import styles from './Alerta.module.css'
const ICONOS = { info: 'ℹ️', exito: '✅', advertencia: '⚠️', error: '⛔' }
export default function Alerta({ tipo = 'info', children }) {
const [visible, setVisible] = useState(true)
if (!visible) return null
return (
<div className={`${styles.alerta} ${styles[tipo]}`} role="alert">
<span>{ICONOS[tipo]}</span>
<div>{children}</div>
<button className={styles.cerrar} onClick={() => setVisible(false)} aria-label="Cerrar">×</button>
</div>
)
}jsx
import { useState } from 'react'
const ENLACES = ['Inicio', 'Servicios', 'Portafolio', 'Contacto']
export default function Navbar() {
const [abierto, setAbierto] = useState(false)
return (
<nav className="border-b bg-white">
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-3">
<span className="text-lg font-bold text-violet-700">MiMarca</span>
<button
className="rounded p-2 md:hidden"
onClick={() => setAbierto((a) => !a)}
aria-expanded={abierto}
aria-label="Abrir menú"
>
☰
</button>
<ul className="hidden gap-6 md:flex">
{ENLACES.map((e) => (
<li key={e}><a href="#" className="hover:text-violet-700">{e}</a></li>
))}
</ul>
</div>
{abierto && (
<ul className="space-y-1 px-4 pb-4 md:hidden">
{ENLACES.map((e) => (
<li key={e}><a href="#" className="block rounded px-2 py-2 hover:bg-gray-100">{e}</a></li>
))}
</ul>
)}
</nav>
)
}Resumen
- CSS Modules (
.module.css): CSS normal con clases únicas por componente. - Tailwind v4:
@tailwindcss/vite+@import "tailwindcss"; personaliza con@theme. - Mobile first con
sm:,md:,lg:; modo oscuro condark:. - Usa
clsx/tailwind-mergepara clases condicionales y nunca construyas clases de Tailwind con variables.