Button · variant=primary
El componente con más tráfico del sistema. Único encargado de comunicar
la acción principal de una pantalla. Existe desde 2019 como <button>
HTML estilizado, y desde noviembre de 2025 está expuesto vía MCP a
agentes externos bajo policy restrictiva. Esta ficha documenta su
estado en mayo de 2026, sus tokens canónicos, sus contratos, y las
cinco variants que el sistema descartó por el camino.
Nº 0247 · Button · variant=primary · size=md · Estado al 2026-05 · Render canónico sobre crema profundo #E8DFCF, sin sombra, sin hover. El estado visible es siempre el de reposo: la pieza en su forma platónica, antes de que la interacción la modifique.
Decomposición del componente
La disciplina del Compendio desmonta cada componente en seis capas sucesivas y después lo vuelve a montar. El Método Mendeléyev exige que esas capas estén nombradas y que nada quede implícito: un agente no puede inferir lo no dicho. El Button primary se atraviesa así.
Las seis capas del componente
01 · Átomo color.red.500 · space.12 · radius.md · duration.120 Tokens primitivos. Valores atómicos sin intención. Si cambian, rompen todo lo que los consume sin excepción. Nunca mutables por agente.
02 · Compuesto color.action.primary.* · space.action.* · button.primary.* Tokens semánticos. Primitivo más intención editorial. El agente los consume, no los declara. Mutables por agente tier-2 bajo policy.
03 · Regla contract test Zod · visual regression Chromatic · OPA policy · OTLP telemetry Governance ejecutable. Cada técnica tiene salida auditable (pass o fail con razón). Forma el colchón sobre el que el componente puede ser mutado sin accidentes.
04 · Pieza <Button variant='primary' size='md' loading=false> El componente con props estables y schema cerrado. Unidad mínima con la que un agente o un humano construye interfaces. Catorce estados catalogados, cinco descartes documentados.
05 · Familia Inputs / Action (17 componentes hermanos) Button es el caso canónico de la familia Inputs / Action. Las variants (secondary, ghost) y los parientes (ButtonGroup, IconButton, LinkButton) comparten tokens, patrones de a11y y policies OPA. La familia crece por consenso editorial, no por acumulación.
06 · Estado reposo · hover · focus-visible · active · disabled · loading · error · success · agent-aware Los comportamientos temporales y contextuales del componente. La textura es lo que el agente puede variar en runtime bajo el Estado 3; lo demás es contract.
Tokens consumidos
El Button primary depende de nueve tokens. Seis son referencias semánticas; tres son tokens de componente derivados. Ningún valor literal vive dentro del componente: todo está aliasado. Esta es la condición mínima para que un agente pueda mutar el componente sin tocar su código.
Semánticos
color.action.primary.bg oklch(0.53 0.18 26) Fondo en reposo
color.action.primary.fg oklch(0.97 0 0) Texto sobre fondo primario, contraste AAA verificado
color.action.primary.border transparent Sin borde visible por diseño. Existe como token para modo high-contrast
space.action.x 1rem Padding horizontal, escala tipográfica Utopia
space.action.y 0.625rem Padding vertical
radius.md 0.5rem Curvatura, token primitivo único para toda la familia Inputs
Componente
button.primary.bg.hover derive(color.action.primary.bg, lightness-5%) Derivado en runtime vía función oklch()
button.primary.focus.ring oklch(0.72 0.15 26 / 0.4) Halo de focus, 4px, visible en todos los modos
button.primary.motion.press { scale: 0.98, duration: 120ms, easing: cubic-bezier(0.2, 0, 0.38, 1) } Única animación permitida. Se desactiva con prefers-reduced-motion
Técnicas de governance aplicadas
Cuatro técnicas activas sobre este componente en mayo de 2026. Todas viven en CI y en el repo público operario-ui. Su salida (éxitos y fallos) es material didáctico: cada rechazo del sistema explica por qué rechaza.
Técnicas activas
Contract test
Cubre reposo, hover, focus-visible, focus-within, active, disabled, loading (con spinner), loading-done, error, success, rtl, long-label, short-label, icon-only. Rechaza combinaciones imposibles: disabled+loading, icon-only sin aria-label.
Visual regression
Baseline revisado cada release. TurboSnap evita re-snapshots de archivos no tocados. Fallos bloquean el merge.
Policy as code
(1) Agente tier-2 puede cambiar variant y size, no tokens. (2) Dos Button primary en el mismo ViewportSegment generan warning. (3) variant=destructive requiere ConfirmModal adyacente en el árbol. (4) Botón en formulario exige type explícito.
Telemetría OTLP
Pendiente de medición real. Cuando el MCP de tresestados.design entre en activo (Fase 3), esta sección reportará renders mensuales y proporción humano vs agente con cifras auditables. Hoy el sitio declara 'agente activo: no' en el colofón, así que cualquier dato aquí sería fabricación. El campo queda como esquema editorial para que la ficha sea legible cuando la cifra exista.
Estado físico
Agentic-consumable desde noviembre de 2025.
Es el cuarto de los cinco estados del ciclo de vida documentados en el Compendio. El Button primary atravesó los anteriores en este orden: diseñado en Figma (2019), tokenizado en JSON (2021), implementado en React (2022), governable (contract tests y visual regression, 2024), agentic-consumable (expuesto vía MCP con schema estricto, 2025-11).
Para pasar al estado plástico o auto-generativo (la pieza del Estado 3) el componente necesitaría además derivación de variants en runtime bajo policies, declaración visible de mutación, rollback automático si la variante generada falla auditoría de accesibilidad. Ese salto está planificado para 2026-Q4.
Estados soportados
hover, focus-visible, active, disabled, loading, reactive-to-context, reactive-to-modality, agent-aware.
La última, agent-aware, es la que distingue al Estado 2. Cuando el componente detecta que su render proviene de una composición generada por agente, emite un atributo data-origin="agent" consumido por la telemetría. No cambia el aspecto visual: la detección es para medición, no para marcar al lector.
Composiciones prohibidas
El schema del Button incluye cinco reglas OPA que rechazan composiciones antes de que lleguen a render. Cada rechazo devuelve un mensaje editorial explicando la razón y, cuando procede, la alternativa canónica.
Reglas activas
Dos Button primary en el mismo ViewportSegment
Dos acciones primarias simultáneas confunden al usuario y rompen el principio Tufte de una sola marca roja. El sistema permite el render pero emite warning en CI. Alternativa: usar una primary y una secondary o ghost.
variant=destructive sin ConfirmModal adyacente
Un Button destructive debe estar ligado a un ConfirmModal en el mismo árbol o a un flujo explícito de confirmación. La policy rechaza la composición si no encuentra el modal. Previene que un agente componga 'borrar cuenta' sin confirmación.
Botón dentro de <form> sin type explícito
El default de un <button> dentro de <form> es type='submit'. Un agente que componga un Button como 'cancelar' sin type='button' dispara el submit. La policy exige type explícito en todo Button hijo de form.
Mutación de tokens primitivos por agente tier-2
Agente tier-2 puede cambiar variant y size del Button, nunca los tokens primitivos (color.red.500). El intento de mutación primitiva se rechaza con 'tier-2 no está autorizado a mutar primitivos; proponer mutación a semantic'.
icon-only sin aria-label
Un Button con solo icono y sin aria-label es invisible para SR. La policy rechaza la composición y sugiere el label accesible basado en la intención semántica del icono.
Interoperabilidad
El Button no vive solo. Su posición en el grafo de composición del sistema define qué puede y no puede hacer.
- Consumido por (componentes padres que lo integran):
Toolbar,CardFooter,ModalFooter,FormRow,EmptyState,ButtonGroup. En cada padre el Button hereda spacing específico pero su comportamiento no cambia. - Consume (componentes hijos):
Icon(opcional, 16×16, alineado al baseline),Spinner(solo en estado loading, 14×14, hereda color del Button). No consume nada más; el Button es hoja en la mayoría de composiciones. - Con qué compone mal (exclusiones documentadas): dentro de
Tooltip(el Tooltip implica información opcional, un Button implica acción; la combinación es ambigua) y dentro deToast(el Toast es efímero, una acción que desaparece en cinco segundos es hostil). Para ambos casos el sistema sugiereConfirmModaloBanner. - Sustituibles por:
IconButtonsi la acción es accesoria y cabe en un icono con aria-label;LinkButtonsi la intención semántica es navegación, no acción.
Medición propuesta
Cuando el sistema entre en Estado 2 activo con MCP público (Fase 3 del roadmap), este Button emitirá los siguientes eventos OTLP hacia Grafana. Hoy están pendientes de activación: el colofón declara “agente activo: no”, así que ninguna cifra se reporta.
Eventos planificados
button.render
Atributos: variant, size, loading, disabled, route, origin (humano | agente tier-1 | agente tier-2). Sin PII. Agregación por ruta y por hora. Permite poda informada de variants poco usadas.
button.click
Atributos: variant, ruta origen, destino si es navegación, latencia percibida si es acción asíncrona. Sin user ID. Permite detectar rutas donde el botón primario se usa menos del 10% de los renders (candidato a revisar jerarquía).
button.error
Log del tipo de error (400, 500, timeout) sin cuerpo. Correlaciona tasas de error con variantes (destructive vs primary) para ajustar confirmaciones.
button.agent_origin
data-origin='agent' se lee de la composición entrante y se propaga al span. La distinción humano vs agente no cambia el render, solo el etiquetado. Permite calcular ratio de renders por tipo de consumidor.
Mutación plástica (Estado 3)
En el Estado 3 plástico, el Button muta en runtime para adaptarse al contexto del lector sin perder su contract. Las reglas de mutación son deterministas y publicadas; el agente solo compone variantes dentro del espacio permitido.
Qué muta y qué permanece
Densidad size = derive(viewport × pointer × reducedMotion) Si el pointer es coarse (táctil) o el viewport es pequeño, size sube a lg para garantizar hit-target 44×44. En desktop con pointer fine, size queda md. Con prefers-reduced-motion activo, no se anima el cambio de tamaño.
Motion motion.press = derive(prefers-reduced-motion) Con reduced-motion, la animación de press (scale 0.98, 120ms) se desactiva por completo. El feedback de click se da por cambio de fondo instantáneo, no por transform.
Tipografía del label label-density = derive(locale × saveData) Idiomas con palabras largas (alemán, holandés) aceptan label más ancho; idiomas compactos (japonés) comprimen. Con saveData activo, el Button no carga variantes tipográficas extra; se queda con el fallback del sistema.
Color tint = derive(hora-local × prefers-contrast) Cromatismo temporal aplica oscilaciones de saturación al rojo accent. Con prefers-contrast more, se bloquea la mutación cromática y se queda en el valor canónico con contraste AAA.
Fijo por contract schema de props · tokens primitivos · aria-label · role Nunca mutan en runtime. El agente no puede cambiar variant=primary a variant=secondary por contexto; no puede eliminar el aria-label por compacidad; no puede mutar el role='button'. Contract es contract.
Si una mutación propuesta rompe policy OPA (por ejemplo, derivar un color fuera del espacio semántico permitido, o reducir hit-target por debajo de 44×44 aunque el contexto lo sugiera), el sistema aplica rollback automático a la variante canónica, emite log auditable con la razón del rechazo y, en desarrollo, muestra un aviso editorial al lector: “el Button 0247 propuso una mutación que no cumplía AA; se sirve la variante canónica”. El lector puede ver qué se rechazó en /estados/estado-3 como demostración de la séptima regla deontológica.
Genealogía
Árbol de evolución
HTML estilizado
Clase CSS .btn-primary sobre <button>. Sin componente. Valores en LESS.
Consumidor Humano diseñador en Figma + humano desarrollador
Componente React + tokens JSON
Primera versión como <Button variant='primary'>. Tokens extraídos a JSON. Sin DTCG; convención interna.
Consumidor Equipo de producto, cuatro productos distintos
Button v2 · variantes y ripple
Se añaden variants: primary, secondary, destructive, ghost, outline. Se añade una animación de ripple que será retirada en 2024 por accesibilidad.
Consumidor Doce productos. Primera documentación pública
Button governable
Contract tests con Zod. Visual regression con Chromatic. Ripple retirado por incompatibilidad con prefers-reduced-motion. Tokens migrados a DTCG 2024.05.
Consumidor Veintinueve productos. Primera auditoría WCAG AAA
Button.AI · slot agéntico
DTCG 2025.10 con $extensions.policy. Expuesto vía MCP con schema estricto. Primera policy OPA: tier-2 no puede mutar primitivos.
Consumidor Humano + agente tier-1 + agente tier-2
Button plástico
planificadoDerivación de size y density en runtime bajo context (viewport × pointer × reducedMotion × saveData). Declaración visible de mutación. Fallback a estado canónico si OPA rechaza.
Consumidor Estado 3 · cualquier cliente MCP · agente lector del sitio
Notas del editor
Descartes catalogados
Cinco variants fueron propuestas, prototipadas o sopesadas a lo largo de los años, y finalmente rechazadas. El catálogo razonado incluye también lo que no entró: aquí están documentadas con su año y el motivo por el que fueron descartadas.
Variants rechazadas
variant='ghost-pressed' Solapamiento semántico con variant='ghost' más pseudo-clase :active. El agente no podía distinguir el estado del componente del propio componente. Se retiró antes de producción.
variant='dual-action' Botón con acción primaria y secundaria simultáneas (un split button). Rompía el principio rector de la familia: un Button comunica una acción, no dos. Se movió a un componente aparte: <ButtonGroup variant='split'>.
variant='destructive-outline' La combinación de alerta (destructive) con ligereza visual (outline) generaba falsos negativos en contraste: los usuarios no percibían la gravedad de la acción. A11y audit lo rechazó. Se mantuvo variant='destructive' sólida.
size='xl' Un size extra-grande para pantallas de coche y visionOS. Terminó convertido en un componente distinto, <ActionTarget size='reachable'>, con lógica propia de hit-target adaptativo según pointer type. Un Button no debe intentar ser todos los botones.
variant='agentic-only' Propuesta de una variant visible únicamente cuando la telemetría detectara que el render venía de un agente. Rechazada por el principio deontológico: el componente no cambia su presencia según quién lo lea. La detección agent-aware es para medición, no para distinción visual.
Referencias cruzadas
Este Button consume y es consumido por otros puntos del corpus:
- Técnica aplicada: Nº 007 · RAG con chunking coherente. Uno de los contratos documentados.
- Caso análogo: Nº 030 · Rescate de un negocio de contenido. Mismo patrón en otro dominio.
- Glosario: Nº 032 · Soberanía del dato y memoria externa.
- Estado al que pertenece principalmente: Estado 2 · Agéntico.
Ficha metadata
- Editor. Joan Arbó.
- Revisión editorial. Pendiente.
- Licencia. CC BY-NC-SA 4.0 para el texto y los diagramas. MIT para el código del repo enlazado.
- Próxima revisión prevista. 2026-09, al cerrar la transición a estado plástico planificada para Q4.
- Cambios desde última revisión. Primera publicación de la ficha. Antes de esta, el componente estaba documentado solo en Storybook interno.