Next.js
Styling en Next.js
Explica cómo organizar CSS global, Modules, Tailwind, Sass y CSS-in-JS en Next.js, incluyendo theming, responsive, streaming y accesibilidad visual.
- Última actualización
- Actualizada
- Nivel
- Fundamentos
Next.js
Explica cómo organizar CSS global, Modules, Tailwind, Sass y CSS-in-JS en Next.js, incluyendo theming, responsive, streaming y accesibilidad visual.
Next.js no impone un sistema de estilos. Organiza cómo CSS entra al grafo, se divide por rutas y convive con Server Components, streaming y producción. La elección entre CSS global, Modules, Tailwind, Sass o CSS-in-JS debe basarse en alcance, runtime, mantenibilidad y compatibilidad con el modelo servidor-cliente.
React decide qué estructura y estados visuales existen. CSS resuelve presentación, layout, responsive, animación y gran parte de la accesibilidad visual.
React / Next.js
→ estructura, datos y variantes
CSS
→ cascada, layout, color, tipografía y adaptación
build tooling
→ imports, bundling, minificación y code splittingNo conviertas decisiones visuales en state o Client Components cuando media queries, selectors y custom properties ya resuelven el problema.
En App Router puedes importar CSS global desde layouts, pages o componentes. El root layout es el lugar común para reset, tokens y estilos de documento:
// app/layout.tsx
import "./globals.css";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es">
<body>{children}</body>
</html>
);
}/* app/globals.css */
@layer reset, tokens, base, components, utilities;
@layer tokens {
:root {
--color-surface: #ffffff;
--color-text: #18181b;
--space-4: 1rem;
--radius-lg: 1rem;
}
}
@layer base {
*, *::before, *::after {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--color-surface);
color: var(--color-text);
}
}Selectores amplios pueden afectar rutas no relacionadas. Usa layers, convenciones y alcance intencional.
/* product-card.module.css */
.card {
display: grid;
gap: 1rem;
border-radius: var(--radius-lg);
}
.title {
font-weight: 600;
}import styles from "./product-card.module.css";
export function ProductCard({ product }: Props) {
return (
<article className={styles.card}>
<h2 className={styles.title}>{product.name}</h2>
</article>
);
}El build transforma nombres para evitar colisiones. Los estilos pueden importarse desde Server o Client Components porque CSS Modules no necesitan ejecutar un runtime de estilos en el navegador.
function Button({ variant, disabled }: ButtonProps) {
const className = [
styles.button,
styles[variant],
disabled && styles.disabled,
]
.filter(Boolean)
.join(" ");
return <button className={className} disabled={disabled} />;
}Una utilidad como clsx mejora lectura, pero no debe esconder combinaciones inválidas. Modela variantes con uniones discriminadas cuando la API lo necesita.
Utility-first expresa estilos cerca del markup:
export function Alert({ tone, children }: AlertProps) {
return (
<div
className={cn(
"rounded-xl border p-4",
tone === "danger" && "border-red-500/30 bg-red-500/10 text-red-950",
tone === "success" && "border-green-500/30 bg-green-500/10 text-green-950",
)}
>
{children}
</div>
);
}// Avoid: compiler cannot see complete class names reliably
`bg-${color}-500`Usa mapas explícitos:
const toneClasses = {
danger: "bg-red-500",
success: "bg-green-500",
};Tailwind es tooling de CSS, no una capacidad de Server Components. Un Server Component puede devolver clases normalmente.
Next.js soporta .scss y .sass después de instalar sass:
pnpm add -D sass.card {
padding: $space-4;
&:hover {
transform: translateY(-2px);
}
}Sass aporta variables, mixins y nesting durante build. CSS moderno ya ofrece custom properties, nesting y layers; usa Sass cuando sus abstracciones reducen duplicación real.
Variables Sass existen solo en build. Custom properties pueden cambiar en runtime.
Existen dos familias:
Generan/injectan estilos en navegador o servidor durante render.
Riesgos con App Router:
Extraen CSS durante compilación. Suelen integrarse mejor con RSC, pero dependen del plugin/bundler.
No asumas que una librería compatible con Pages Router funciona igual en App Router. Consulta su guía oficial para streaming y useServerInsertedHTML cuando corresponda.
Autores de librerías CSS-in-JS pueden registrar estilos antes de contenido que los usa:
"use client";
import { useServerInsertedHTML } from "next/navigation";
export function StyleRegistry({ children }: { children: React.ReactNode }) {
useServerInsertedHTML(() => {
return <style>{/* collected server styles */}</style>;
});
return children;
}No necesitas esta API para CSS global, Modules o Tailwind. Es una herramienta especializada.
Una estrategia robusta usa tokens CSS y un atributo:
:root {
--surface: white;
--text: #18181b;
}
[data-theme="dark"] {
--surface: #09090b;
--text: #fafafa;
}<html data-theme={theme}>prefers-color-scheme).Si el servidor no conoce la preferencia y el cliente cambia el atributo después de hidratar, puede existir flash o mismatch.
Para evitarlo:
Trade-off: leer cookie vuelve dinámica la región/documento. Puedes mantener shell pública y aplicar CSS de sistema cuando no necesitas persistencia servidor.
Para layout visual:
.grid {
display: grid;
grid-template-columns: 1fr;
}
@media (min-width: 48rem) {
.grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}No uses useEffect + window.innerWidth para cambiar columnas. Crea hydration mismatch, listeners y renders.
Usa JavaScript cuando cambia comportamiento o datos, no solo presentación.
Container queries permiten que un componente responda a su contenedor en vez del viewport.
Diseña:
React comunica estados mediante atributos/classes:
<button data-loading={pending} aria-busy={pending}>CSS los presenta:
.button[data-loading="true"] {
cursor: wait;
}No bases significado únicamente en color.
CSS no convierte un componente en cliente. Puedes importar un Module en un Server Component.
La boundary cambia cuando usas:
Mantén primitives visuales servidor cuando no necesitan esas capacidades.
El orden de CSS puede depender del orden de imports y del bundling. Reglas:
En desarrollo y producción el chunking puede diferir; prueba build real.
Un editor o datepicker puede requerir CSS global:
import "vendor-library/styles.css";Colócalo en el layout mínimo que usa la librería si el framework/package lo permite. Revisa:
No copies todo el CSS a globals para modificar una regla sin entender actualizaciones.
Next.js divide CSS según imports y rutas. Una page no debería descargar estilos de features nunca visitadas.
Pero demasiados archivos pequeños pueden aumentar requests/overhead según build y HTTP.
Mide:
Prefiere transform y opacity para animaciones suaves. Respeta:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
scroll-behavior: auto !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
}
}No apliques una regla global agresiva si rompe indicadores necesarios; diseña una variante reducida.
Animaciones de entrada no deben retrasar la visibilidad del LCP.
globals.css
→ reset, tokens, typography
CSS Modules or Tailwind
→ components and pages
root data-theme
→ appearance
next/font variables
→ font tokensProjectCard usa aspect ratio, focus-visible y reduced motion. El layout responsive vive en CSS; el estado del proyecto llega desde React.
Visual regression tests ayudan, pero no sustituyen teclado y lectores.
Hydration y trabajo innecesario.
Colisiones y dependencia implícita.
Flash/orden incorrecto en streaming.
Flash y mismatch.
CSS ausente en producción.
Resultados diferentes por chunking.
Empeora percepción y métricas.
bg-${color}-500 puede fallar en Tailwind?Lazy loading, Suspense y streaming explica cómo dividir código y revelar contenido sin convertir cada espera en un spinner global.