Módulo 7: Primitives And Component Libraries

Cuándo usar una librería y cuándo construir

Descripción

Después de cinco lecciones hablando de primitivas, hace falta la pregunta que las pone en su lugar: ¿todo necesita una? No. Un Badge que muestra "Envío gratis" en una tarjeta de producto no tiene foco que atrapar, ni teclas que manejar más allá de las que el navegador ya resuelve solo, ni un rol ARIA exótico —es texto con color, y el variants() del módulo 5 lo resuelve completo—. Instalar una primitiva para eso sería, en el mejor caso, código de más, y en el peor, una capa de indirección sin ningún beneficio real. Esta lección da el criterio para decidir, componente por componente, si lo construyes con lo que ya sabes o si te apoyas en una primitiva.

Conexión con el módulo. Cierra el argumento de "cuándo" antes de que la lección 7 se meta a fondo en el caso más exigente (Dialog). Reusa la idea de la lección 1 —patrón de teclado no trivial + manejo de foco como las señales que empujan hacia una primitiva— y la convierte en un motor de decisión ejecutable, aplicado a los componentes reales que Mercado ya tiene o va a necesitar.

Una analogía: cuándo llamas a un electricista y cuándo cambias tú el foco

Cambiar un foco fundido en tu casa es una tarea que casi cualquiera hace sin pensarlo dos veces: apagas el interruptor, desenroscas, enroscas el nuevo, listo. Nadie llama a un electricista para eso —sería un desperdicio de tiempo y dinero para un trabajo de riesgo prácticamente nulo—. Pero instalar un tablero eléctrico nuevo, o cablear un cuarto adicional, es una historia distinta: hay normas de seguridad exactas, un error puede causar un incendio, y el margen de "casi lo hice bien" no existe —está bien o hay un riesgo real—. Ahí sí llamas a alguien certificado, no porque seas incapaz de aprenderlo, sino porque el costo de un error es alto y el problema ya está resuelto, de forma segura y probada, por alguien que se especializa en eso.

Un Badge o una Card son el foco fundido: los cambias tú, con variants(), sin pensarlo dos veces —el "riesgo" de hacerlo mal es bajo, y ya sabes cómo—. Un Dialog o un Combobox son el tablero eléctrico: el patrón de teclado tiene reglas exactas (qué tecla hace qué, en qué orden, con qué excepciones), y hacerlo mal no se nota a simple vista —la lección 2 ya lo mostró— hasta que alguien que depende de ese comportamiento se topa con la falla. Ahí no reinventas: te apoyas en la primitiva, que ya resolvió el problema de forma probada.

Guarda la imagen: cambiar un foco lo haces tú; cablear un tablero lo hace quien ya resolvió ese riesgo. La pregunta no es "¿puedo aprenderlo?", es "¿vale la pena reinventar algo que ya está resuelto y donde el error es difícil de notar?"

Ejemplo trabajado: un motor de decisión con dos señales

El criterio completo cabe en dos preguntas: ¿el componente tiene un patrón de teclado no trivial (algo más que "clic lo activa": flechas, Escape, combinaciones)? ¿El componente necesita manejar el foco activamente (atraparlo, moverlo, devolverlo)? Si la respuesta a cualquiera de las dos es sí, la señal apunta a usar una primitiva. Si las dos son no, constrúyelo — el riesgo de hacerlo mal es bajo y el trabajo de una primitiva sería, para ese caso, sobreingeniería.

// L6 - cuando construyes y cuando usas una primitiva. un motor de decision chico,
// no un tribunal: dos senales bastan para la mayoria de los casos.

function recommend(component) {
  const { name, hasComplexKeyboardPattern, hasFocusManagement } = component;
  if (hasComplexKeyboardPattern || hasFocusManagement) {
    const reasons = [];
    if (hasComplexKeyboardPattern) reasons.push('patron de teclado no trivial (flechas, escape, o ambos)');
    if (hasFocusManagement) reasons.push('maneja el foco (trap, retorno, o autofocus)');
    return { name, decision: 'usa una primitiva', reason: reasons.join(' + ') };
  }
  return { name, decision: 'constrúyelo', reason: 'un rol y un clic alcanzan; nada de foco ni teclado que resolver' };
}

const components = [
  { name: 'Button',       hasComplexKeyboardPattern: false, hasFocusManagement: false },
  { name: 'Badge',        hasComplexKeyboardPattern: false, hasFocusManagement: false },
  { name: 'Card',         hasComplexKeyboardPattern: false, hasFocusManagement: false },
  { name: 'Tooltip',      hasComplexKeyboardPattern: false, hasFocusManagement: true },
  { name: 'Dialog',       hasComplexKeyboardPattern: true,  hasFocusManagement: true },
  { name: 'DropdownMenu', hasComplexKeyboardPattern: true,  hasFocusManagement: true },
  { name: 'Combobox',     hasComplexKeyboardPattern: true,  hasFocusManagement: true },
];

console.log('=== construir vs usar una primitiva ===\n');
console.log('componente'.padEnd(15) + 'decision'.padEnd(20) + 'por que');
console.log('-'.repeat(75));
for (const c of components) {
  const { name, decision, reason } = recommend(c);
  console.log(name.padEnd(15) + decision.padEnd(20) + reason);
}

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

=== construir vs usar una primitiva ===

componente     decision            por que
---------------------------------------------------------------------------
Button         constrúyelo         un rol y un clic alcanzan; nada de foco ni teclado que resolver
Badge          constrúyelo         un rol y un clic alcanzan; nada de foco ni teclado que resolver
Card           constrúyelo         un rol y un clic alcanzan; nada de foco ni teclado que resolver
Tooltip        usa una primitiva   maneja el foco (trap, retorno, o autofocus)
Dialog         usa una primitiva   patron de teclado no trivial (flechas, escape, o ambos) + maneja el foco (trap, retorno, o autofocus)
DropdownMenu   usa una primitiva   patron de teclado no trivial (flechas, escape, o ambos) + maneja el foco (trap, retorno, o autofocus)
Combobox       usa una primitiva   patron de teclado no trivial (flechas, escape, o ambos) + maneja el foco (trap, retorno, o autofocus)

Recorre la tabla de arriba abajo y fíjate en el punto de quiebre. Los primeros tres —Button, Badge, Card— son exactamente los componentes que ya construiste en el módulo 5 con variants(), sin ningún problema, y el motor confirma lo que ya sabías por experiencia: no tienen nada de comportamiento complejo que resolver. Tooltip es un caso intermedio interesante: no tiene un patrón de teclado elaborado, pero sí necesita manejo de foco (mostrarse al enfocar el elemento que describe, ocultarse al perder el foco, no interceptar el Tab) — y esa sola señal ya empuja hacia una primitiva. Los últimos tres —Dialog, DropdownMenu, Combobox— son los que vas a construir en el proyecto (los dos primeros) o los que quedan como ejercicio para explorar (Combobox): todos combinan teclado complejo y manejo de foco, la peor combinación para reinventar a mano.

Nota algo importante sobre el motor: son dos señales, no una lista fija de nombres de componentes. Eso significa que el criterio se sostiene aunque mañana Mercado necesite un componente que esta lista no menciona —un Accordion, un Slider, un Tabs—. Pregúntate las dos mismas cosas: ¿el teclado hace algo más que "clic lo activa"? ¿hay foco que mover o atrapar? Si alguna es sí, ya sabes hacia dónde mirar.

Profundización: por qué el costo de reinventar no es obvio hasta que lo intentas

La razón por la que este criterio importa —y no es solo "usa primitivas siempre por las dudas"— es que el costo de construir mal un componente de teclado complejo no se ve al escribir el código; se ve semanas después, en un caso borde que nadie probó. Un DropdownMenu casero puede funcionar perfecto en la demo: se abre con clic, muestra las opciones, se cierra al elegir una. Los casos que rompen ese "funciona perfecto" solo aparecen cuando alguien navega distinto de como tú probaste: ¿qué pasa si el usuario presiona la flecha hacia abajo dos veces y luego Home para ir al primer ítem? ¿Y si presiona una letra para saltar al ítem que empieza con esa letra (el patrón estándar de menús)? ¿Y si el foco está en el último ítem y presiona flecha abajo — se queda ahí, o vuelve al primero (wrap-around)? Cada una de esas preguntas tiene una respuesta definida en la especificación APG del patrón Menu Button — y una primitiva ya las resolvió todas, probadas contra ese documento. Reconstruirlas a mano no es imposible, pero es, literalmente, reimplementar una especificación completa por componente, cada vez que el equipo necesita un menú nuevo.

Esto no significa "nunca construyas nada de teclado" — significa que el criterio de las dos señales es una forma barata de anticipar ese costo antes de escribir el componente, no después de descubrir el caso borde en producción. Y hay un matiz final que vale la pena decir con honestidad: la línea entre "trivial" y "no trivial" no siempre es nítida — un Tabs con solo dos pestañas fijas es casi tan simple como un Button; un Tabs con pestañas dinámicas, navegación con flechas y Home/End ya cruza la línea. El motor de esta lección es una guía, no una ley — cuando dudes, la pregunta de respaldo es la de la lección 1: ¿esto es comportamiento (posible reinvención cara) o apariencia (tuya, siempre)?

El punto de quiebre, visualmente:

  sin teclado complejo, sin foco que manejar   →  CONSTRUYE  (Button, Badge, Card)
  con foco que manejar, sin teclado complejo   →  PRIMITIVA  (Tooltip)
  con teclado complejo (con o sin foco)        →  PRIMITIVA  (Dialog, Menu, Combobox, Tabs)

Errores comunes

Usar una primitiva para absolutamente todo, "por si acaso". Qué pasa: se envuelve hasta el Badge más simple en una primitiva, agregando una dependencia y una capa de indirección donde variants() solo ya resolvía todo. Por qué pasa: después de ver el poder de las primitivas en este módulo, se siente más seguro usarlas siempre. Cómo detectarlo: tu Badge importa una primitiva pero nunca usa nada de foco, teclado o estado que esa primitiva resuelve — la estás usando solo por el nombre del elemento HTML que envuelve. Cómo corregirlo: aplica las dos señales del motor de esta lección. Si las dos son "no", el variants() del módulo 5 sobre un elemento nativo simple (<span>, <div>) es la solución completa, más simple y sin dependencia extra.

Subestimar un componente "simple" que en realidad tiene teclado complejo. Qué pasa: se construye un Tabs a mano pensando "solo son botones que cambian de contenido", y se olvida que el patrón APG de Tabs exige navegación con flechas entre las pestañas y Home/End para saltar a la primera/última. Por qué pasa: visualmente un Tabs se parece a un grupo de botones, y el patrón de teclado no es evidente hasta que lo buscas. Cómo detectarlo: tu Tabs casero funciona con clic pero un usuario de teclado solo puede llegar con Tab a cada pestaña una por una, en vez de entrar una vez al grupo y moverse con flechas. Cómo corregirlo: antes de construir, revisa el patrón APG del widget que vas a hacer (recursos de esta lección) — si tiene una sección de "Keyboard Interaction" con más de dos o tres teclas, esa es la señal de "usa una primitiva" que el motor de esta lección buscaba capturar.

Rechazar toda primitiva porque "agrega una dependencia". Qué pasa: se evita Radix por regla general, con el argumento de "menos dependencias es mejor", y se termina reinventando un Combobox accesible desde cero. Por qué pasa: minimizar dependencias es, en general, un buen instinto — pero aplicado sin criterio a componentes de comportamiento complejo. Cómo detectarlo: tu equipo pasó más tiempo depurando el focus-trap de un Combobox casero que el que hubiera tomado instalar y vestir la primitiva. Cómo corregirlo: la lección 5 ya mostró que la primitiva bien elegida (Radix como dependencia, estilo copiado) no ata tu proyecto a un look ajeno ni a una API que no controlas — el costo real de esa dependencia es bajo, y el ahorro en comportamiento correcto (foco, teclado, ARIA) es alto para exactamente los componentes que este criterio marca.

Ejercicios

Ejercicio 1 — Aplica el motor. Sin correr nada, clasifica cada componente como "constrúyelo" o "usa una primitiva", con la señal que te lleva a esa decisión:

  • (a) Un Avatar que muestra la foto o las iniciales de un usuario.
  • (b) Un Accordion de preguntas frecuentes, donde cada sección se expande/colapsa con clic y flechas, y solo una puede estar abierta a la vez.
  • (c) Un Skeleton (el placeholder gris que "carga" mientras llega el contenido real).
  • (d) Un Popover que se abre al hacer clic en un ícono de información y se cierra con Escape o clic afuera.
Ver solución
  • (a) Constrúyelo. Un Avatar es una imagen o texto con estilo; no tiene interacción ni foco que manejar.
  • (b) Usa una primitiva. Navegación con flechas entre secciones y manejo de qué está expandido son exactamente las dos señales (teclado complejo + estado que coordinar).
  • (c) Constrúyelo. Un Skeleton es puramente visual —ni siquiera es interactivo—; variants() con una animación de pulso lo resuelve completo.
  • (d) Usa una primitiva. Escape para cerrar y clic-afuera-cierra son comportamiento de foco/dismissal, la misma familia de problemas que un Dialog — de hecho Radix ofrece Popover como primitiva hermana de Dialog.

Ejercicio 2 — Predice la salida. Sin correr nada: si agregaras { name: 'Accordion', hasComplexKeyboardPattern: true, hasFocusManagement: false } a la lista de components del ejemplo trabajado, ¿qué línea imprimiría recommend() para Accordion?

Ver solución
Accordion      usa una primitiva   patron de teclado no trivial (flechas, escape, o ambos)

Solo aparece la primera razón en el reason, porque hasFocusManagement es false — el reasons.push de esa condición nunca se ejecuta. La decisión sigue siendo "usa una primitiva" porque el if usa || (basta con que una señal sea verdadera), pero el texto de "por qué" refleja honestamente cuál de las dos señales fue la que disparó la recomendación — no inventa una razón que no aplica.

Ejercicio 3 — El límite del motor. El motor de esta lección tiene solo dos señales booleanas. ¿Qué tipo de decisión no podría capturar bien, y por qué está bien que el motor no lo intente?

Ver solución

El motor no puede capturar matices de grado — por ejemplo, "¿qué tan complejo es el teclado?" es una pregunta de sí/no en el modelo, pero en la realidad hay grises (un Tabs de dos pestañas fijas vs. uno con pestañas dinámicas y reordenables). Tampoco captura contexto de negocio — dos equipos con el mismo componente podrían decidir distinto según cuánto tiempo tienen, cuánta gente en el equipo ya conoce Radix, o qué tan crítico es ese componente específico para el producto. Y está bien que no lo intente: la profundización de esta misma lección lo dice explícitamente — el motor es una guía barata para el caso común, no un árbitro final. Cuando el caso es ambiguo, la decisión la toma una persona, con más contexto del que dos booleanos pueden cargar; el motor solo te ahorra pensarlo desde cero en los casos donde la respuesta es clara.

Resumen y siguiente paso

En esta lección cerraste la pregunta de "cuándo": si un componente tiene un patrón de teclado no trivial o necesita manejar el foco activamente, usa una primitiva; si ninguna de las dos aplica, constrúyelo con variants() como ya sabes hacerlo. Con el foco fundido y el tablero eléctrico viste la lógica detrás del criterio: no es cuestión de capacidad, es cuestión de si el problema ya está resuelto, de forma probada, y si el costo de un error es alto y difícil de notar. Y lo comprobaste ejecutando: siete componentes reales de Mercado, clasificados con solo dos señales, con el punto de quiebre exactamente donde la intuición del módulo lo esperaba.

Antes de avanzar deberías poder: aplicar las dos señales a un componente que no está en la lista del ejemplo; explicar por qué el costo de reinventar mal un widget de teclado no se ve hasta que alguien lo navega distinto de como tú lo probaste; y reconocer que el motor es una guía, no una ley absoluta.

Ya tienes el criterio completo del módulo: por qué existen las primitivas (L1-L2), cómo están hechas (L3), cómo se visten (L4), de dónde sale el código (L5), y cuándo usarlas (L6). La lección 7 toma el componente que el motor de esta lección marcó, sin dudar, como el candidato más claro a primitiva —el Dialog— y lo lleva hasta el fondo: vas a auditar, con a11yAudit() completo, un Dialog casero contra uno construido sobre Radix, y vas a ver el número exacto que separa a los dos.

Recursos

  • W3C WAI-ARIA APG, "Patterns" (índice completo) — w3.org/WAI/ARIA/apg/patterns. El catálogo de patrones con su sección de "Keyboard Interaction" —la fuente para juzgar si un componente tiene teclado "no trivial"—. En inglés.
  • Radix Primitives, "Introduction" (sección de componentes disponibles) — radix-ui.com/primitives/docs/overview/introduction. La lista completa de qué primitivas existen ya resueltas —tu primer lugar para revisar antes de construir algo desde cero—. En inglés.
  • Headless UI, "Introduction" — headlessui.com. Una alternativa a Radix con la misma filosofía —comportamiento sin estilo—, mantenida por el equipo de Tailwind; vale la pena conocerla como opción. En inglés.
  • shadcn/ui, "Components" (índice) — ui.shadcn.com/docs/components. Qué componentes ya vienen resueltos con este modelo, antes de decidir construir uno desde cero. En inglés.