Skip to content

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.

EnfoqueCómo funcionaProsContras
CSS globalimport './App.css'SimpleChoques de nombres
CSS Modulesimport s from './X.module.css'CSS normal, clases aisladasArchivo extra por componente
TailwindClases utilitarias en classNameMuy rápido, consistente, sin CSS muertoJSX con muchas clases
Estilos en líneastyle con un objetoValores dinámicosSin pseudo-clases ni media queries
CSS-in-JS (styled-components)CSS dentro de JSDinámicoCosto 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 clsx
jsx
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/vite
js
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 ​

NecesidadClases
Espaciadop-4, px-6, mt-2, gap-4, space-y-2
Tamañow-full, h-12, max-w-md, min-h-screen
Flex / Gridflex items-center justify-between, grid grid-cols-3
Textotext-sm, font-bold, text-gray-600, text-center
Fondo y bordebg-white, border, rounded-xl, shadow-md
Estadoshover:, focus-visible:, active:, disabled:
Responsive (mobile first)sm:, md:, lg:, xl:
Modo oscurodark:

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-merge
js
// 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 ​

  1. Con CSS Modules, crea un componente Alerta con variantes info, exito, advertencia y error, con ícono y botón para cerrar.
  2. Con Tailwind, crea un Navbar responsive: en móvil muestra un botón de hamburguesa que despliega el menú; desde md muestra 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 con dark:.
  • Usa clsx/tailwind-merge para clases condicionales y nunca construyas clases de Tailwind con variables.

Academia Web — aprende, copia y construye.