Modal · diálogo bloqueante
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
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
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
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
Obligatorio. Sin focus trap el Modal degrada a caja flotante. Rechazado en CI.
Modal anidado dentro de Modal
Sin excepciones. El segundo cierra el primero automáticamente.
Modal con contenido informativo no accionable
Un Modal que solo muestra información sin requerir acción se rechaza. Alternativa: Drawer, Popover o route dedicada.
Modal sin labelledby o describedby
El SR anuncia el dialog por su label. Sin label accesible se rechaza.
Interoperabilidad
- Consumido por:
ConfirmFlow,AuthGuard,ErrorBoundary(nivel crítico). - Consume:
Headingcomo title (obligatorio),Buttonprimary + secondary en footer,FormRowopcional 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:
Drawersi el flujo es extenso;Popoversi el contenido es contextual;Bannersi la información persiste pero no bloquea.
Medición propuesta
Eventos planificados
modal.open
Atributos: causa (destructive, auth, error), ruta origen, origin (humano | agente). Detecta rutas con apertura frecuente (candidato a flujo mejorado).
modal.dismiss_without_action
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
- Familia: Overlays.
- Componente relacionado: Nº 0257 · Toast.