0247
Inputs / Action Creacion Revision

Button · variant=primary

Publicado
Lectura
6 min

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

Herramienta
Storybook Interactions + Zod
Cobertura
14 estados

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

Herramienta
Chromatic TurboSnap
Cobertura
Light + dark · 3 viewports

Baseline revisado cada release. TurboSnap evita re-snapshots de archivos no tocados. Fallos bloquean el merge.

Policy as code

Herramienta
OPA + Rego (WASM)
Cobertura
4 policies activas

(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

Herramienta
OpenTelemetry → Grafana
Cobertura
ejemplo ilustrativo

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

Herramienta
OPA · warning level
Cobertura
Cualquier composición de página

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

Herramienta
OPA · block level
Cobertura
Árbol de composición

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

Herramienta
OPA · block level
Cobertura
Formularios

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

Herramienta
OPA · block level
Cobertura
Schema $extensions.policy

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

Herramienta
OPA · block level + axe-core
Cobertura
WCAG SC 4.1.2

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 de Toast (el Toast es efímero, una acción que desaparece en cinco segundos es hostil). Para ambos casos el sistema sugiere ConfirmModal o Banner.
  • Sustituibles por: IconButton si la acción es accesoria y cabe en un icono con aria-label; LinkButton si 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

Herramienta
OTLP · OpenTelemetry Metrics
Cobertura
Cada render del componente en producción

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

Herramienta
OTLP · OpenTelemetry Events
Cobertura
Cada interacción confirmada

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

Herramienta
OTLP · OpenTelemetry Logs
Cobertura
Acción que devuelve 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

Herramienta
OTLP · OpenTelemetry Attributes
Cobertura
Detección agent-aware

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

planificado

Derivació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:

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.