Saltar al contenido

Arquitectura CSS a gran escala: del caos a la confianza

Cómo estructurar el CSS en aplicaciones grandes: tokens de diseño, patrones de componentes, utilidades y los principios que evitan la entropía.

4 min de lectura
Hojas de estilo enredadas y etiquetas de anulación se organizan en una estructura ordenada de tokens de diseño, componentes reutilizables y utilidades.

El problema del CSS a gran escala

El CSS es fácil de escribir y difícil de mantener. Cada desarrollador añade estilos que funcionan de forma aislada y, con los meses, la hoja de estilos acumula miles de líneas de overrides, código muerto y batallas de especificidad.

La solución no es un nuevo framework de CSS: es un enfoque arquitectónico claro que siga todo el equipo.

Capa 1: Tokens de diseño — la base

Los tokens de diseño son la única fuente de verdad para los valores visuales. Sin ellos, el mismo color, espaciado o radio aparece como valor hardcodeado en decenas de lugares, lo que hace imposible aplicar cambios consistentes.

csscss
/* tokens.css — global design decisions */
:root {
  /* Color palette — raw values */
  --color-neutral-50: #fafafa;
  --color-neutral-900: #171717;
  --color-blue-500: #3b82f6;
  --color-blue-600: #2563eb;
 
  /* Semantic tokens — what the color means, not what it is */
  --color-text-primary: var(--color-neutral-900);
  --color-text-secondary: #6b7280;
  --color-action-primary: var(--color-blue-600);
  --color-action-primary-hover: var(--color-blue-500);
  --color-background-page: #ffffff;
  --color-background-subtle: var(--color-neutral-50);
 
  /* Dark mode overrides semantic tokens only */
  @media (prefers-color-scheme: dark) {
    --color-text-primary: var(--color-neutral-50);
    --color-background-page: var(--color-neutral-900);
  }
}

La regla: los componentes usan tokens semánticos, nunca valores en bruto. Cambiar un color de marca se convierte en la actualización de un único token.

Capa 2: Tailwind para componentes utility-first

El enfoque utility-first de Tailwind elimina por construcción los conflictos de nombres y los estilos sin usar. Pero a gran escala exige disciplina.

Extracción de componentes vs. proliferación de utilidades

tsxtsx
// ❌ Utility sprawl — unreadable, hard to maintain
function Button({ children, variant }: ButtonProps) {
  return (
    <button
      className={`
      inline-flex items-center justify-center gap-2 rounded-md px-4 py-2
      text-sm font-medium transition-colors focus-visible:outline-none
      focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2
      disabled:pointer-events-none disabled:opacity-50
      ${
        variant === "primary"
          ? "bg-blue-600 text-white hover:bg-blue-700 active:bg-blue-800"
          : "border border-gray-200 bg-white text-gray-900 hover:bg-gray-50"
      }
    `}
    >
      {children}
    </button>
  );
}
 
// ✅ Extracted with cva (class-variance-authority)
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
 
const buttonVariants = cva(
  // Base classes — always applied
  "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      variant: {
        primary: "bg-primary text-primary-foreground hover:bg-primary/90",
        secondary:
          "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        destructive:
          "bg-destructive text-destructive-foreground hover:bg-destructive/90",
      },
      size: {
        sm: "h-8 px-3 text-xs",
        md: "h-9 px-4",
        lg: "h-11 px-6 text-base",
      },
    },
    defaultVariants: { variant: "primary", size: "md" },
  },
);
 
function Button({ className, variant, size, ...props }: ButtonProps) {
  return (
    <button
      className={cn(buttonVariants({ variant, size }), className)}
      {...props}
    />
  );
}

Capa 3: La utilidad cn() — combinar clases de forma segura

Al combinar clases condicionales, usa clsx + tailwind-merge para evitar conflictos.

tstypescript
// lib/utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
 
export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
 
// Without cn — later classes don't override earlier ones
const class1 = "px-4 px-8"; // Both px-4 and px-8 exist — undefined behavior
 
// With cn — tailwind-merge resolves conflicts
const class2 = cn("px-4", "px-8"); // → "px-8" — last value wins correctly
 
// Usage in components
function Card({ className, elevated }: CardProps) {
  return (
    <div
      className={cn(
        "rounded-lg border bg-card p-6",
        elevated && "shadow-lg",
        className // Allow consumers to override
      )}
    />
  );
}

Capa 4: Espaciado y layout consistentes

El espaciado inconsistente es la forma más rápida de que una UI se vea descuidada. Impone una escala de espaciado.

csscss
/* Spacing scale — multiples of 4px */
:root {
  --space-1: 0.25rem; /* 4px */
  --space-2: 0.5rem; /* 8px */
  --space-3: 0.75rem; /* 12px */
  --space-4: 1rem; /* 16px */
  --space-6: 1.5rem; /* 24px */
  --space-8: 2rem; /* 32px */
  --space-12: 3rem; /* 48px */
  --space-16: 4rem; /* 64px */
}

Con Tailwind, esto se impone mediante la escala de espaciado en tailwind.config.ts. La regla: los valores de espaciado que no están en la escala no están permitidos.

Capa 5: Diseño de la API de componentes

Una buena arquitectura CSS se refleja en la API de los componentes.

tsxtsx
// ❌ Style leaks through className everywhere
<Section className="mt-12 mb-8 px-4 max-w-4xl mx-auto">
  <Heading className="text-2xl font-bold mb-4">Title</Heading>
  <Text className="text-gray-600 leading-relaxed">Content</Text>
</Section>
 
// ✅ Semantic props — consumers express intent, not implementation
<Section spacing="lg" contained>
  <Heading level={2} size="xl">Title</Heading>
  <Text color="secondary" size="base">Content</Text>
</Section>

Las props semánticas te permiten refactorizar la implementación de CSS sin tocar a todos los consumidores.

Mantenerlo sano con el tiempo

Audita el CSS muerto: usa PurgeCSS o la purga integrada de Tailwind. Los estilos sin usar son complejidad invisible.

Prohíbe los números mágicos: si aparece margin-top: 17px en una PR, pregunta por qué no es space-4 (16px). Los números mágicos rompen el sistema de diseño.

Un archivo de componente, un archivo de estilos: coloca los estilos junto a los componentes. Las hojas de estilo globales que crecen hasta miles de líneas son imposibles de mantener.

Documenta las decisiones: lo más importante de un sistema de diseño no son los componentes, es el registro de decisiones. ¿Por qué el botón tiene 8px de padding vertical? ¿Por qué el radio de borde es 6px? Documéntalo para que los futuros colaboradores no tengan que adivinar.

La arquitectura CSS no es un trabajo glamuroso, pero un sistema de diseño bien estructurado es una de las inversiones con mayor apalancamiento que puede hacer un equipo de frontend. Los mejores son invisibles: simplemente hacen que cada componente nuevo sea rápido de construir y consistente con todo lo demás.

Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX