Por qué las variables CSS genéricas terminan siendo deuda técnica
El antipatrón clásico: empezás con --blue, --dark-blue, --lighter-blue. Funciona en el MVP. Seis meses después tenés --blue-for-buttons, --blue-but-darker, --new-blue y --blue-final-v2. Sin convención de nombres escalable, cada variable nueva es una decisión ad-hoc que el próximo dev interpretará diferente.
Los mejores design systems usan dos capas de variables: primitivas y semánticas. Primitivas son los valores raw: --color-blue-500: #3b82f6. Semánticas mapean intención: --color-primary: var(--color-blue-500). Esto permite cambiar el azul de toda la app modificando una línea. Pero acá está el truco: las primitivas deben seguir escalas numéricas (50-900 estilo Tailwind) o t-shirt sizing (xs-xl), nunca --color-blue-kinda-light.
Un equipo de design ops de Shopify documentó que el 70% de los bugs visuales después de rebrand venían de colores hardcoded en vez de usar variables semánticas. Alguien ponía color: #3b82f6 directo en CSS en vez de color: var(--color-primary). Cuando cambiaron el branding, tuvieron que hacer grep por 847 archivos buscando ese hex específico. Solución: linter que bloquee valores raw en favor de custom properties.
Escalas de spacing que no rompen en responsive sin media queries
El error más común con spacing: definir --spacing-small: 8px, --spacing-medium: 16px, --spacing-large: 24px y después sorprenderse que se ve mal en mobile. Spacing fijo no escala. Los mejores sistemas usan clamp() para spacing fluido: --spacing-md: clamp(1rem, 2vw, 1.5rem). Crece con el viewport sin media queries.
La escala numérica (4px, 8px, 12px, 16px, 24px, 32px...) es predecible pero rígida. T-shirt sizing (xs, sm, md, lg, xl) es más semántico pero requiere consenso: ¿tu 'md' es 16px o 20px? Equipos grandes combinan ambas: --spacing-4 (primitiva) + --spacing-md (semántica que mapea a --spacing-4). Así podés refactorear la escala sin romper componentes.
Un gotcha de spacing: no uses las mismas variables para padding y gap. Gap de flexbox/grid se comporta diferente que padding (colapsa en bordes). Mejor tener --gap-sm separado de --spacing-sm, o al menos documentar claramente cuál es para qué. Y nunca, NUNCA definas --spacing-default sin número — 'default' no comunica escala ni predecibilidad.
Typography tokens que sobreviven al agregado de nuevas fonts
El antipatrón: --font-heading, --font-body, --font-small. ¿Qué pasa cuando querés agregar una font display para hero sections, o una mono para code blocks? Terminás con --font-heading-v2 o --font-hero-special. Mejor separar family, size y weight en variables independientes: --font-family-sans, --font-size-lg, --font-weight-bold. Combinás en el uso: font: var(--font-weight-bold) var(--font-size-lg) / var(--line-height-tight) var(--font-family-sans).
Para font-size, la escala numérica falla en comunicar intención. ¿--font-size-5 es grande o chico? Mejor: --font-size-sm, --font-size-base, --font-size-lg, --font-size-xl, --font-size-2xl... hasta --font-size-5xl para hero headings. Y agregá variantes fluid: --font-size-fluid-lg: clamp(1.25rem, 2vw + 1rem, 2rem). Se adapta solo de mobile a desktop.
Line-height es donde más fallan los sistemas. Definir --line-height: 1.5 global no funciona — headings necesitan tighter (1.2), body text necesita relaxed (1.6), buttons necesitan 1. Mejor: --line-height-tight, --line-height-normal, --line-height-relaxed. Y nunca uses unidades en line-height (ej 24px) — siempre unitless (1.5) para que escale proporcionalmente al font-size.
Errores comunes que rompen la herencia de custom properties
Custom properties heredan por el cascade de CSS, pero muchos devs las usan como si fueran variables de preprocesador. Ejemplo común: definís --color-text: black en :root, pero después en un componente dark hacés background: black; color: white hardcoded. El texto dentro hereda --color-text: black y se vuelve invisible. Solución: siempre redefine las variables en el contexto: .dark { --color-text: white; --color-bg: black; }.
Otro gotcha: custom properties no funcionan en media queries. No podés hacer @media (min-width: var(--breakpoint-md)). Las variables solo funcionan en valores de propiedades, no en at-rules. Tenés que definir los breakpoints como constantes aparte o usar PostCSS custom media queries. Un workaround común es tener --breakpoint-md como documentación pero usarlo hardcoded en el @media.
El error más sutil: usar fallbacks incorrectos. var(--color-primary, blue) parece seguro, pero si --color-primary está definida como invalid o un valor que el browser no entiende, el fallback NO se usa — el browser usa el valor de herencia o initial. Mejor práctica: siempre definí todas las variables custom en :root aunque sea con placeholder, nunca dependas del fallback de var() para valores críticos. Y usá @supports para detectar si custom properties están disponibles antes de depender de ellas.