0253
Overlays Creacion Revision

Modal · diálogo bloqueante

Publicado
Lectura
5 min

El Modal interrumpe. Esa es su esencia y su pecado. Solo se usa para decisiones que requieren atención exclusiva inmediata: confirmación destructiva, autenticación, error bloqueante. Para cualquier otra cosa hay alternativas mejores: Drawer, Toast, página dedicada. Esta ficha documenta cuándo el Modal es la respuesta y cuándo es el problema.

Decomposición del componente

Las seis capas del componente

01 · Átomo color.neutral.950 · opacity.55 · space.16 · radius.md · duration.200

Primitivos. La opacity 55% del backdrop es el único valor de transparencia que el sistema permite en overlays bloqueantes.

02 · Compuesto color.surface.modal.bg · color.surface.backdrop · space.modal.padding · size.modal.maxwidth · motion.modal.enter

Semánticos. maxwidth 32rem es el único ancho canónico; ampliar indica que el contenido pertenece a una route dedicada.

03 · Regla focus trap + inert · policy restrictiva de uso · restauración del foco previo · WAI-ARIA dialog

Governance crítica. Sin focus trap, el Modal no es Modal.

04 · Pieza <Modal open={x} onClose={fn} labelledby={id}>

Schema que impone labelledby obligatorio y onClose idempotente. Tres casos permitidos: confirmación destructiva, autenticación, error bloqueante.

05 · Familia Overlays · sibling de Drawer, Popover, Toast, Tooltip, Banner

El Modal es el overlay más restrictivo. Drawer para flujos extensos laterales; Popover para contenido contextual; Toast para feedback efímero.

06 · Estado opening · open · confirming · closing · agent-aware

confirming es el estado intermedio cuando el usuario ha actuado pero la operación aún no se resuelve.

Tokens consumidos

Semánticos

color.surface.modal.bg oklch(0.99 0.005 75)

Fondo del modal sobre el dim del backdrop

color.surface.backdrop oklch(0 0 0 / 0.55)

Backdrop semitransparente

space.modal.padding 2rem

Padding interno generoso

size.modal.maxwidth 32rem

Ancho máximo, 512px en base 16

motion.modal.enter { duration: 200ms, easing: cubic-bezier(0.2, 0, 0.38, 1) }

Animación de entrada, opacidad y scale 0.98 a 1, deshabilitada con prefers-reduced-motion

Técnicas de governance aplicadas

Técnicas activas

Focus trap obligatorio

Herramienta
focus-trap-react + restauración del foco previo
Cobertura
WAI-ARIA dialog modal

El foco no puede salir del modal mientras está abierto. Tab y Shift+Tab ciclan dentro. Al cerrar, el foco vuelve al elemento que lo abrió. Escape cierra siempre.

Inert para el resto del DOM

Herramienta
atributo HTML inert
Cobertura
Todo el árbol fuera del modal

Mientras el modal está abierto, el resto del DOM se marca como inert: no recibe foco, ni lectura por SR, ni clicks. Sustituye al hack histórico de aria-hidden + tabindex -1.

Policy de uso restrictiva

Herramienta
OPA policy
Cobertura
3 casos permitidos, resto rechazados

Casos permitidos: confirmación destructiva, autenticación bloqueante, error que requiere acción inmediata. El agente que componga un Modal para mostrar contenido informativo o un formulario opcional recibe rechazo con sugerencia de Drawer o página dedicada.

Estado físico

Agentic-consumable desde noviembre de 2025. La policy OPA es estricta porque el Modal es el componente más fácil de abusar, especialmente por agentes que aprendieron de patrones marketing donde aparecen para newsletter signup.

Estados soportados

open, closing (durante animación de salida), confirming (intermedio mientras espera respuesta del usuario), agent-aware. Sin estados hover/focus en el contenedor (el contenido interno tiene las suyas).

Composiciones prohibidas

Reglas activas

Modal sin focus trap

Herramienta
OPA · block level
Cobertura
WAI-ARIA dialog

Obligatorio. Sin focus trap el Modal degrada a caja flotante. Rechazado en CI.

Modal anidado dentro de Modal

Herramienta
OPA · block level
Cobertura
Un único dialog abierto

Sin excepciones. El segundo cierra el primero automáticamente.

Modal con contenido informativo no accionable

Herramienta
OPA · block level
Cobertura
Casos permitidos: destructive, auth, error bloqueante

Un Modal que solo muestra información sin requerir acción se rechaza. Alternativa: Drawer, Popover o route dedicada.

Modal sin labelledby o describedby

Herramienta
OPA · block level
Cobertura
SC 4.1.2

El SR anuncia el dialog por su label. Sin label accesible se rechaza.

Interoperabilidad

  • Consumido por: ConfirmFlow, AuthGuard, ErrorBoundary (nivel crítico).
  • Consume: Heading como title (obligatorio), Button primary + secondary en footer, FormRow opcional para inputs de confirmación.
  • Con qué compone mal: con otro Modal simultáneo, con Tooltip anidado (colisión de overlay), con Toast (el Toast se cierra si aparece Modal).
  • Sustituibles por: Drawer si el flujo es extenso; Popover si el contenido es contextual; Banner si la información persiste pero no bloquea.

Medición propuesta

Eventos planificados

modal.open

Herramienta
OTLP · Events
Cobertura
Cada apertura

Atributos: causa (destructive, auth, error), ruta origen, origin (humano | agente). Detecta rutas con apertura frecuente (candidato a flujo mejorado).

modal.dismiss_without_action

Herramienta
OTLP · Metrics
Cobertura
Cerrado sin elegir

Ratio de modales cerrados con Escape o backdrop sin acción. Alto ratio indica que el Modal no aportaba.

Mutación plástica (Estado 3)

Qué muta y qué permanece

Tamaño width = derive(viewport × content)

Mobile full-width con 16px de margen; desktop fijo en maxwidth 32rem; tablet escala entre ambos.

Animación de entrada motion = derive(prefers-reduced-motion)

Con reduced-motion, sin scale ni fade: aparece instantáneo.

Botón primario por defecto focus-default = derive(intent)

Si el intent es destructive, el foco inicial cae en Cancelar (no en Confirmar). Para auth, cae en el input de autenticación.

Fijo por contract focus trap · inert resto del DOM · labelledby · Escape cierra · restauración del foco al origen

Inviolables.

Rollback si la mutación propone desactivar focus trap, abrir dos Modal simultáneos, o servir un Modal informativo que podría ser Drawer. La policy nunca cede en casos bloqueantes.

Genealogía

Árbol de evolución

Modal con div + display

Sin focus trap. Sin restauración. Bug recurrente: foco perdido tras cerrar.

Consumidor Producto · 8 productos

Modal con WAI-ARIA dialog

Implementación correcta del pattern. Focus trap. Aria-hidden en el resto.

Consumidor Producto · 22 productos

Modal governable

Migración de aria-hidden hack a atributo HTML inert nativo. Policy OPA restrictiva.

Consumidor Producto · 30 productos

Modal.AI · expuesto vía MCP

Schema con casos permitidos enumerados. Agente que proponga Modal para contenido informativo recibe rechazo.

Consumidor Humano + agente

Notas del editor

Descartes catalogados

Variants rechazadas

variant='soft-modal'

Modal sin backdrop ni focus trap, solo overlay visual. No era modal: era una caja flotante. Movido a componente <Card floating /> separado.

variant='nested'

Modal dentro de Modal. Imposible de gestionar con SR sin perder contexto. Sustituido por la regla de un solo modal abierto a la vez; el segundo cierra el primero.

variant='full-bleed'

Modal a pantalla completa indistinguible de página. Rompía la expectativa de cerrar y volver al contexto. Promovido a Route component cuando el contenido lo requería.

Referencias cruzadas