Módulo 1: What A Design System Is

Consistencia y el costo del drift

Descripción

La consistencia es una de esas palabras que todos aprueban y casi nadie define. "La UI debe ser consistente" suena a obviedad, a buen gusto, a algo estético y opinable. Esta lección hace lo contrario: convierte la consistencia en algo medible, y a su enemigo —el drift— en un número que puedes calcular. Cuando la consistencia deja de ser una opinión ("me parece que se ve disparejo") y se vuelve un dato ("hay 3 valores distintos para un rol que debería tener 1"), tienes un argumento duro para montar un sistema, y una forma de saber si tu sistema está funcionando.

La tesis de la lección es que el drift tiene un costo, y ese costo es contable en tres monedas: se paga en confianza (una UI dispareja se siente descuidada, poco profesional), en tiempo (cada divergencia es una decisión que alguien tuvo que tomar y que alguien tendrá que reconciliar), y en bugs (dos valores que deberían ser uno son dos cosas que pueden romperse por separado). Vamos a auditar el storefront de Mercado en su versión ad-hoc, medir su drift a través de varios roles a la vez, y ponerle una cifra: el drift score.

Conexión con el módulo. La lección 1 midió el drift de un solo rol (el color); la 2, de otro (el espaciado). Esta lección generaliza: audita varios roles a la vez y suma el caos en una sola métrica, mostrando que el drift no es un accidente aislado sino una propiedad del sistema entero. Y conecta directo con la 3: el drift es, exactamente, lo que pasa cuando la capa de tokens no existe y cada componente escribe sus valores a mano. La lección 5 tomará el otro lado de la misma moneda —cómo el sistema no solo previene el drift, sino que hace barato el cambio (una única fuente de verdad)—.

Una analogía: la orquesta afinando

Imagina una orquesta de treinta músicos a punto de tocar. Cada instrumento, por sí solo, suena bien: el violinista afinó su violín, el oboísta su oboe, el chelista su chelo. Cada uno está "correcto" en aislamiento.

Pero si cada quien afinó por su cuenta —uno con un diapasón viejo, otro de oído, otro con una app— sus "la" no son exactamente el mismo "la". Uno afinó a 440 Hz, otro a 442, otro a 438. Individualmente, imperceptible. Juntos, cuando los treinta tocan la misma nota, el oído siente algo mal aunque no sepa nombrarlo: la nota está "sucia", tiene un batido, se oye barata. Nadie tocó una nota equivocada; el problema es que las notas correctas no coinciden entre sí. Eso es el drift: no errores, sino verdades que no concuerdan.

Por eso las orquestas afinan todas al mismo la antes de empezar: el oboe da un "la", y los treinta se afinan a ese. No a "un la bueno" cada uno, sino al mismo la, una sola referencia. Ese "la" único es el token. La consistencia de la orquesta no viene de que cada músico sea disciplinado; viene de que todos apuntan a la misma referencia. Y el costo de no hacerlo no es que un instrumento suene mal —suenan bien por separado—; es que el conjunto suena barato. Igual que una UI: cada pantalla se ve bien, pero el recorrido completo se siente descuidado, y el usuario lo percibe aunque no sepa por qué.

De cerca: las tres monedas del drift

El drift no es un problema estético abstracto. Cuesta, concretamente, en tres cosas.

Cuesta confianza. Un usuario no analiza tu CSS, pero siente la incoherencia. Botones que casi son iguales, espacios que casi coinciden, azules que casi combinan: el cerebro registra el "casi" como descuido, y el descuido como poca profesionalidad. En un storefront como Mercado, donde el usuario está a punto de dar su tarjeta, esa sensación de "algo no está pulido" es dinero que se va. La consistencia es, en parte, una promesa: "esto está cuidado, puedes confiar".

Cuesta tiempo. Cada valor divergente representa una decisión que alguien tomó (mal, de memoria) y una decisión que alguien tendrá que deshacer después. Cuando el equipo decide "unifiquemos los botones", el drift acumulado se convierte en horas de cacería: buscar cada azul, cada padding, cada radio disperso, y reconciliarlos. El tiempo que el ad-hoc "ahorró" al escribirse se paga con intereses al limpiarse.

Cuesta bugs. Dos valores que deberían ser uno son dos cosas que pueden romperse por separado. Si el color de foco de los botones está escrito a mano en cinco lugares, y en cuatro se corrige un problema de contraste pero en el quinto se olvida, ahí quedó un bug de accesibilidad que nadie ve hasta que un usuario con baja visión no puede usar ese botón. Una única fuente de verdad no puede tener este bug: se corrige en un lugar y vale para todos. El drift multiplica la superficie donde las cosas pueden salir mal.

Las tres monedas comparten una raíz: el drift es tener más de una verdad para una sola idea. Medirlo es, entonces, contar cuántas verdades hay donde debería haber una.

Ejemplo trabajado: auditar el drift de todo un storefront

Hasta ahora medimos un rol a la vez. Un storefront real tiene muchos roles divergiendo en paralelo: el color de marca, el espacio entre tarjetas, el radio de las esquinas, y una docena más. Vamos a auditar tres a la vez y a resumir el caos en una sola métrica.

La idea del drift score: para cada rol, lo ideal es 1 valor. Todo lo que sobre de 1 es drift. Si un rol tiene 3 valores distintos, aporta 3 - 1 = 2 de drift (dos decisiones de más que nadie debería haber tomado). Sumamos ese exceso a través de todos los roles y obtenemos un número: cuántas decisiones sobrantes carga el storefront. Normalizamos las unidades (como en la lección 2) para no confundir 1rem con 16px:

// L4 — el costo del drift: auditamos 3 roles en el storefront ad-hoc y contamos
// cuantas "decisiones distintas" existen para lo que deberia ser UNA sola.
const adHoc = {
  'color.primary': ['#3b82f6', '#2563eb', '#3b82f6', '#3c83f6'],
  'space.card-gap': ['16px', '1rem', '18px', '16px'],
  'radius.card': ['8px', '6px', '8px', '0.5rem'],
};

function normalize(role, value) {
  if (role.startsWith('space') || role.startsWith('radius')) {
    return value.endsWith('rem') ? Math.round(parseFloat(value) * 16) + 'px' : value;
  }
  return value.toLowerCase();
}

console.log('=== Auditoria de drift del storefront ad-hoc ===\n');
let driftScore = 0;
for (const role of Object.keys(adHoc)) {
  const distinct = [...new Set(adHoc[role].map((v) => normalize(role, v)))];
  driftScore += distinct.length - 1; // 1 = lo ideal; lo que sobra es drift
  console.log(role.padEnd(16) + distinct.length + ' valores distintos  ' + JSON.stringify(distinct));
}
console.log('\nDrift score (decisiones de sobra): ' + driftScore);
console.log('Con un sistema, cada rol = 1 token  ->  drift score: 0');

Qué esperar. Al correr el archivo, la salida es exactamente esta:

=== Auditoria de drift del storefront ad-hoc ===

color.primary   3 valores distintos  ["#3b82f6","#2563eb","#3c83f6"]
space.card-gap  2 valores distintos  ["16px","18px"]
radius.card     2 valores distintos  ["8px","6px"]

Drift score (decisiones de sobra): 4
Con un sistema, cada rol = 1 token  ->  drift score: 0

Léela como un diagnóstico médico del storefront. Cada línea es un rol y su nivel de "infección":

color.primary → 3 valores distintos. De los cuatro azules escritos (#3b82f6, #2563eb, #3b82f6, #3c83f6), tres son realmente distintos —#3b82f6 se repite, pero #2563eb y #3c83f6 son otros—. Tres verdades para "el azul de marca". Aporta 3 - 1 = 2 al drift score.

space.card-gap → 2 valores distintos. El 1rem normalizó a 16px (falso problema, se fusiona con los 16px), pero el 18px es un valor de verdad distinto. Quedan dos: 16px y 18px. Aporta 2 - 1 = 1.

radius.card → 2 valores distintos. El 0.5rem normalizó a 8px (se fusiona con los 8px), pero el 6px es distinto. Quedan 8px y 6px. Aporta 2 - 1 = 1.

Suma total: 2 + 1 + 1 = 4. El storefront ad-hoc carga 4 decisiones de sobra —cuatro divergencias que nadie eligió y que alguien tendrá que reconciliar—. Ese número es el costo del drift hecho métrica. No es una opinión ("se ve disparejo"); es un hecho contable, y puedes vigilarlo en el tiempo: si sube, tu UI se está desordenando; si es 0, cada rol tiene una sola verdad.

Y la última línea es la promesa del sistema: con tokens, cada rol es un solo token, así que cada rol tiene exactamente 1 valor, y el drift score es 0 por construcción. No "0 si el equipo es disciplinado", sino 0 estructuralmente: no hay dónde divergir. Ese es el argumento duro. La consistencia no es un ideal al que aspiras con esfuerzo; es el estado por defecto cuando los valores salen de tokens.

Una pregunta para que la lleves a la siguiente lección: el drift score mide el desorden actual. Pero hay un segundo costo, más caro todavía: cuando quieres cambiar uno de estos roles a propósito (digamos, oscurecer el azul de marca), el ad-hoc te obliga a editar cada lugar a mano. ¿Cuántas ediciones? Eso lo mide la lección 5.

Un matiz: consistencia no es uniformidad

Cuidado con una lectura simplona de todo esto. "Consistencia" no significa que todo sea igual —eso sería una UI plana y aburrida—. Significa que lo que cumple el mismo rol sea igual, y que lo que cumple roles distintos difiera de forma sistemática.

Un botón primario y un botón secundario deben verse distintos: uno es la acción principal, otro la alternativa. Eso no es drift; es una variante, una diferencia intencional que el sistema codifica (lección 5). El drift es lo contrario: cuando dos cosas del mismo rol —dos botones primarios, dos gaps entre tarjetas— difieren sin intención. La prueba para distinguirlos: pregúntate "¿alguien decidió esta diferencia, y significa algo?". Si sí, es variación intencional (bien). Si "simplemente salió así", es drift (mal). Un sistema maduro tiene mucha variación —variantes, escalas, temas— pero cero drift: toda diferencia es a propósito.

Errores comunes

Tratar la consistencia como cuestión de gusto. Qué pasa: cuando alguien señala que la UI se ve dispareja, la respuesta es "a mí me parece que está bien" o "es subjetivo". Por qué pasa: sin una métrica, la consistencia parece opinión. Cómo detectarlo: las discusiones sobre "¿se ve bien?" no terminan nunca porque no hay un dato que las cierre. Cómo corregirlo: mide. Cuenta cuántos valores distintos hay para cada rol. "El azul de marca tiene 3 valores" no es opinión; es un hecho, y su corrección (unificar en un token) es objetiva. El drift score convierte una pelea estética en una tarea técnica con un criterio de "listo": drift score 0.

Confundir drift con variación intencional. Qué pasa: en el afán de "eliminar inconsistencias", alguien hace que el botón primario y el secundario se vean idénticos, matando una diferencia que debía existir. Por qué pasa: se lee "consistencia" como "todo igual". Cómo detectarlo: tu UI perdió jerarquía —ya no se distingue la acción principal de la secundaria— en nombre de la uniformidad. Cómo corregirlo: la meta es cero drift (diferencias sin intención), no cero diferencias. Las diferencias que significan algo (primario vs secundario, título vs cuerpo) son variantes del sistema y son buenas. Solo se eliminan las diferencias que nadie eligió. Consistencia es "lo del mismo rol, igual"; no "todo, igual".

Creer que el drift se arregla "teniendo cuidado". Qué pasa: ante el drift, la solución propuesta es "seamos más disciplinados, usemos siempre el mismo valor". Por qué pasa: se ve el drift como un problema de descuido humano. Cómo detectarlo: cada cierto tiempo hay una "campaña de limpieza" de estilos, y al poco tiempo el drift volvió. Cómo corregirlo: la disciplina humana no escala —con suficientes pantallas y personas, alguien escribirá 15px de memoria—. La única solución que escala es estructural: que el valor no se pueda escribir a mano, porque sale de un token que se referencia. No pidas cuidado; quita la oportunidad de equivocarse. Un sistema no confía en la memoria; la vuelve innecesaria.

Ejercicios

Ejercicio 1 — Calcula el drift score. Sin correr nada, calcula el drift score de esta auditoría de dos roles. Recuerda: por rol, valores distintos - 1, y normaliza rem a px (base 16):

const audit = {
  'color.text':   ['#111827', '#111827', '#1f2937', '#111827'],
  'space.section': ['32px', '2rem', '32px', '40px', '2.5rem'],
};
Ver solución
  • color.text: valores #111827, #111827, #1f2937, #111827 → distintos: #111827 y #1f29372. Aporta 2 - 1 = 1.
  • space.section: normalizando (2rem → 32, 2.5rem → 40): 32, 32, 32, 40, 40 → distintos: 32px y 40px2. Aporta 2 - 1 = 1.

Drift score = 1 + 1 = 2.

Nota lo que la normalización hizo en space.section: de los cinco valores escritos (32px, 2rem, 32px, 40px, 2.5rem) que a ojo parecían "cuatro o cinco cosas distintas", solo hay dos valores reales (32 y 40); los rem eran las mismas medidas escritas distinto. El drift real (2) era menor que el aparente, pero sigue siendo drift: dos verdades donde el sistema tendría una (space-8 = 32px, digamos) o dos tokens intencionales si 32 y 40 fueran roles distintos.

Ejercicio 2 — ¿Drift o variante? Para cada diferencia observada en el storefront, di si es drift (divergencia sin intención, a corregir) o variante (diferencia intencional, a conservar):

  • (a) El botón "Agregar al carrito" es azul; el botón "Cancelar" es gris.
  • (b) El gap entre tarjetas es 16px en home y 15px en el catálogo.
  • (c) El título de la página usa text-xl; el nombre del producto usa text-lg.
  • (d) El radio de las esquinas del ProductCard es 8px en tres pantallas y 6px en una.
Ver solución
  • (a) Variante (conservar). Azul vs gris para primario vs secundario es una diferencia intencional: comunica jerarquía (acción principal vs alternativa). Es el sistema de variantes funcionando, no drift.
  • (b) Drift (corregir). 16px vs 15px para el mismo rol (el gap entre tarjetas) es una divergencia que nadie eligió —el 15px salió de escribir de memoria—. Unificar en un token (space-4).
  • (c) Variante (conservar). Tamaños distintos para el título de página vs el nombre del producto reflejan una jerarquía intencional (uno es más importante que el otro). Salen ambos de la escala tipográfica; son roles distintos, no el mismo rol divergiendo.
  • (d) Drift (corregir). El mismo componente (ProductCard) con radios distintos entre pantallas es divergencia sin intención. El 6px es el intruso. Unificar en radius-md.

La prueba, otra vez: ¿la diferencia significa algo y alguien la decidió? (a) y (c) sí → variantes. (b) y (d) no, "salió así" → drift.

Ejercicio 3 — Las tres monedas. Para este escenario, identifica en cuál(es) de las tres monedas —confianza, tiempo, bugs— se está pagando el drift, y explica cómo un sistema lo evitaría:

"En el storefront de Mercado, el color de foco de los botones (el borde que aparece al navegar con teclado) está escrito a mano en 6 archivos. La semana pasada se descubrió que en 2 de ellos el foco casi no se ve —contraste insuficiente—. Se corrigió en esos 2. Hoy un usuario reportó que en el checkout el foco sigue sin verse."

Ver solución

Se están pagando las tres monedas, y el escenario las ilustra en cadena:

  • Bugs. El síntoma directo es un bug de accesibilidad: el foco no se ve en el checkout. Existe porque el valor estaba escrito a mano en 6 lugares —6 verdades para un solo rol ("el color de foco")—. Con una única fuente de verdad, este bug es imposible: hay un solo --color-focus, se corrige una vez y vale para los 6.
  • Tiempo. La corrección "en esos 2" fue trabajo manual, y encima incompleto —quedaron 4 sin revisar, uno de los cuales acaba de reventar—. Cada ronda de arreglo es una cacería por todos los archivos, y siempre existe el riesgo de que se escape alguno. Ese tiempo (el de arreglar, más el de volver a arreglar lo que se escapó) es el costo en tiempo del drift.
  • Confianza. Un foco que no se ve en el checkout —la pantalla donde el usuario da su tarjeta— es justo donde la sensación de "esto no está pulido" más caro cuesta. El usuario que reportó el problema ya perdió algo de confianza en que el sitio está cuidado.

Cómo lo evita un sistema: el color de foco es un token (color.focus), aplicado por el componente Button una sola vez. No hay 6 lugares; hay 1. El bug de contraste se corrige en el token, y los 6 usos —incluido el checkout— quedan corregidos a la vez, sin cacería y sin olvidos. El drift no es que "se les olvidó un archivo"; es que había 6 archivos donde debía haber 1.

Resumen y siguiente paso

En esta lección convertiste la consistencia de una opinión en una medida. Con la orquesta afinando viste que el drift no son errores —cada instrumento suena bien solo— sino verdades que no concuerdan, y que el conjunto "suena barato" aunque nadie toque una nota equivocada. Aprendiste las tres monedas del costo del drift —confianza, tiempo y bugs— y su raíz común: tener más de una verdad para una sola idea. Lo mediste ejecutando: auditar tres roles del storefront ad-hoc dio un drift score de 4 —cuatro decisiones de sobra que nadie eligió—, y con tokens ese score es 0 por construcción, no por disciplina. Y afinaste el concepto: consistencia no es uniformidad; la meta es cero drift (diferencias sin intención), no cero diferencias (las variantes intencionales son buenas).

Antes de avanzar deberías poder: calcular un drift score dado un conjunto de roles y sus valores; nombrar las tres monedas del costo del drift; distinguir drift de variación intencional; y explicar por qué "tener cuidado" no escala y la estructura sí.

La lección 5 mira el otro lado de la misma moneda. El drift score mide el desorden que ya existe; pero el argumento más fuerte del sistema no es solo que previene el drift, sino que hace el cambio barato: cuando quieres cambiar un valor a propósito, el ad-hoc te cobra N ediciones (y el riesgo de olvidar alguna, como en el ejercicio del foco), mientras que el sistema te cobra 1. Es la mantenibilidad y la única fuente de verdad, y la vas a medir: un cambio, N actualizaciones automáticas.

Recursos

  • web.dev, "Color and contrast accessibility" — web.dev/articles/color-and-contrast-accessibility. Por qué el contraste (uno de los roles que el drift puede romper, como en el ejercicio del foco) importa y cómo se mide; base del módulo 4. En inglés.
  • Nielsen Norman Group, "Consistency and Standards" — nngroup.com/articles/consistency-and-standards. El principio de usabilidad detrás de por qué la incoherencia "cuesta confianza"; el lado humano del drift. En inglés.
  • Design Tokens Community Group (W3C) — tr.designtokens.org/format. La capa de tokens que lleva el drift score a 0 por construcción; la solución estructural al costo del drift. En inglés.
  • Material Design 3, "Design tokens" — m3.material.io/foundations/design-tokens/overview. Cómo un sistema real usa tokens para garantizar consistencia a escala; un ejemplo del "una sola verdad por rol". En inglés.