Ir al contenido

Modal

.modal muestra una ventana sobre la página, con un fondo oscuro detrás. Necesita ds-uikit.js.

  • .modal lleva hidden, role="dialog", aria-modal="true" y aria-labelledby con el id del título.
  • Dentro van el fondo (modal__backdrop) y la ventana (modal__dialog), con modal__header (título y botón para cerrar), modal__body y modal__footer.
  • El botón que lo abre lleva data-ds-toggle="modal" y data-ds-target="#id". Los que lo cierran, data-ds-dismiss="modal".
<button type="button" class="btn btn--primary" data-ds-toggle="modal" data-ds-target="#modal-basico">Abrir modal</button>
<div class="modal" id="modal-basico" role="dialog" aria-modal="true" aria-labelledby="modal-basico-titulo" hidden>
<div class="modal__backdrop"></div>
<div class="modal__dialog">
<div class="modal__header">
<h2 class="modal__title" id="modal-basico-titulo">Guardar cambios</h2>
<button type="button" class="modal__close" data-ds-dismiss="modal" aria-label="Cerrar"></button>
</div>
<div class="modal__body">
<p>Tienes cambios sin guardar. ¿Quieres guardarlos antes de salir?</p>
</div>
<div class="modal__footer">
<button type="button" class="btn" data-ds-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn--primary">Guardar</button>
</div>
</div>
</div>

Ubica el modal al final del <body>, fuera de elementos con transform, filter o z-index propio: así cubre toda la pantalla. Queda sobre el header fijo (--ds-z-modal, 1050) y bajo los toasts y el loader.

Por defecto la ventana mide hasta 512 px. modal--sm la deja en 320 px y modal--lg en 800 px. En pantallas angostas ocupa el ancho disponible. Para otro ancho, cambia --ds-modal-width.

<button type="button" class="btn btn--primary" data-ds-toggle="modal" data-ds-target="#modal-sm">Pequeño</button>
<button type="button" class="btn btn--primary" data-ds-toggle="modal" data-ds-target="#modal-lg">Grande</button>
<div class="modal modal--sm" id="modal-sm" role="dialog" aria-modal="true" aria-labelledby="modal-sm-titulo" hidden>
<div class="modal__backdrop"></div>
<div class="modal__dialog">
<div class="modal__header">
<h2 class="modal__title" id="modal-sm-titulo">Modal pequeño</h2>
<button type="button" class="modal__close" data-ds-dismiss="modal" aria-label="Cerrar"></button>
</div>
<div class="modal__body">
<p>320 px de ancho máximo.</p>
</div>
</div>
</div>
<div class="modal modal--lg" id="modal-lg" role="dialog" aria-modal="true" aria-labelledby="modal-lg-titulo" hidden>
<div class="modal__backdrop"></div>
<div class="modal__dialog">
<div class="modal__header">
<h2 class="modal__title" id="modal-lg-titulo">Modal grande</h2>
<button type="button" class="modal__close" data-ds-dismiss="modal" aria-label="Cerrar"></button>
</div>
<div class="modal__body">
<p>800 px de ancho máximo. En pantallas más angostas ocupa el ancho disponible.</p>
</div>
</div>
</div>

modal--centered centra la ventana en la pantalla.

<button type="button" class="btn btn--primary" data-ds-toggle="modal" data-ds-target="#modal-centrado">Abrir modal centrado</button>
<div class="modal modal--centered" id="modal-centrado" role="dialog" aria-modal="true" aria-labelledby="modal-centrado-titulo" hidden>
<div class="modal__backdrop"></div>
<div class="modal__dialog">
<div class="modal__header">
<h2 class="modal__title" id="modal-centrado-titulo">Sesión por vencer</h2>
<button type="button" class="modal__close" data-ds-dismiss="modal" aria-label="Cerrar"></button>
</div>
<div class="modal__body">
<p>Tu sesión vence en 5 minutos. ¿Quieres seguir conectado?</p>
</div>
<div class="modal__footer">
<button type="button" class="btn" data-ds-dismiss="modal">Cerrar sesión</button>
<button type="button" class="btn btn--primary" data-ds-dismiss="modal">Seguir conectado</button>
</div>
</div>
</div>

Si el contenido no cabe en la pantalla, el cuerpo hace scroll y la cabecera y el pie quedan a la vista.

<button type="button" class="btn btn--primary" data-ds-toggle="modal" data-ds-target="#modal-largo">Ver términos</button>
<div class="modal" id="modal-largo" role="dialog" aria-modal="true" aria-labelledby="modal-largo-titulo" hidden>
<div class="modal__backdrop"></div>
<div class="modal__dialog">
<div class="modal__header">
<h2 class="modal__title" id="modal-largo-titulo">Términos y condiciones</h2>
<button type="button" class="modal__close" data-ds-dismiss="modal" aria-label="Cerrar"></button>
</div>
<div class="modal__body">
<h3>1. Uso del servicio</h3>
<p>El servicio se entrega tal como está. Puedes usarlo en tus proyectos personales y comerciales, siempre que respetes estas condiciones.</p>
<h3>2. Cuenta</h3>
<p>Eres responsable de mantener la confidencialidad de tu contraseña y de toda la actividad que ocurra en tu cuenta.</p>
<h3>3. Pagos</h3>
<p>Los planes se cobran por adelantado. Puedes cancelar en cualquier momento; el plan sigue activo hasta el fin del período pagado.</p>
<h3>4. Datos personales</h3>
<p>Tratamos tus datos según nuestra política de privacidad. No vendemos tu información a terceros.</p>
<h3>5. Cambios</h3>
<p>Podemos actualizar estos términos. Te avisaremos por correo con al menos 30 días de anticipación.</p>
</div>
<div class="modal__footer">
<button type="button" class="btn" data-ds-dismiss="modal">Rechazar</button>
<button type="button" class="btn btn--primary" data-ds-dismiss="modal">Aceptar</button>
</div>
</div>
</div>

modal__dialog puede ser un <form>: así el botón de enviar del pie queda dentro del formulario. Con autofocus, el campo recibe el foco al abrir.

<button type="button" class="btn btn--primary" data-ds-toggle="modal" data-ds-target="#modal-contacto">Escríbenos</button>
<div class="modal" id="modal-contacto" role="dialog" aria-modal="true" aria-labelledby="modal-contacto-titulo" hidden>
<div class="modal__backdrop"></div>
<form class="modal__dialog" action="/contacto" method="post">
<div class="modal__header">
<h2 class="modal__title" id="modal-contacto-titulo">Escríbenos</h2>
<button type="button" class="modal__close" data-ds-dismiss="modal" aria-label="Cerrar"></button>
</div>
<div class="modal__body">
<div class="form-field">
<label class="form-label" for="contacto-nombre">Nombre</label>
<input class="form-input" id="contacto-nombre" name="nombre" type="text" autocomplete="name" autofocus>
</div>
<div class="form-field">
<label class="form-label" for="contacto-correo">Correo</label>
<input class="form-input" id="contacto-correo" name="correo" type="email" autocomplete="email">
</div>
<div class="form-field">
<label class="form-label" for="contacto-mensaje">Mensaje</label>
<textarea class="form-input" id="contacto-mensaje" name="mensaje" rows="3"></textarea>
</div>
</div>
<div class="modal__footer">
<button type="button" class="btn" data-ds-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn--primary">Enviar</button>
</div>
</form>
</div>

En React, autoFocus no deja el atributo en el DOM (enfoca al montar, con el modal oculto): usa data-autofocus, como en la pestaña React.

Con data-ds-backdrop="static", un clic en el fondo no cierra el modal; Escape y los botones sí. Úsalo cuando cerrar por error haga perder datos.

<button type="button" class="btn btn--primary" data-ds-toggle="modal" data-ds-target="#modal-estatico">Abrir modal</button>
<div class="modal" id="modal-estatico" role="dialog" aria-modal="true" aria-labelledby="modal-estatico-titulo" data-ds-backdrop="static" hidden>
<div class="modal__backdrop"></div>
<div class="modal__dialog">
<div class="modal__header">
<h2 class="modal__title" id="modal-estatico-titulo">Elige una opción</h2>
<button type="button" class="modal__close" data-ds-dismiss="modal" aria-label="Cerrar"></button>
</div>
<div class="modal__body">
<p>Un clic en el fondo no cierra este modal. Usa los botones o Escape.</p>
</div>
<div class="modal__footer">
<button type="button" class="btn" data-ds-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn--primary" data-ds-dismiss="modal">Continuar</button>
</div>
</div>
</div>
  • Al abrir: bloquea el scroll de la página, atrapa el foco dentro del modal y enfoca el elemento con autofocus (o data-autofocus) o, si no hay, el primer elemento enfocable.
  • Se cierra con los botones data-ds-dismiss="modal", con un clic en el fondo (salvo con data-ds-backdrop="static") y con Escape. El foco vuelve al botón que lo abrió.
  • Aparece y desaparece con un fundido y un leve desplazamiento. Queda oculto con hidden, sin quitarlo del DOM.
Atributo Qué hace
data-ds-toggle="modal" En el botón: abre o cierra el modal indicado en data-ds-target (en un enlace, también sirve href="#id").
data-ds-dismiss="modal" Cierra el modal que contiene al botón.
data-ds-backdrop="static" En .modal: un clic en el fondo no lo cierra.
const confirmar = dsUIKit.modal('#confirmar'); // elemento o selector
confirmar.open();
confirmar.close();
confirmar.toggle();
confirmar.isOpen; // true o false
// Pedir confirmación antes de cerrar un formulario con cambios
document.querySelector('#modal-contacto').addEventListener('ds:close', (e) => {
if (hayCambios && !confirm('¿Descartar los cambios?')) e.preventDefault();
});

En React, los atributos funcionan igual. Para abrirlo desde código: modal(ref.current).open() (con import { modal } from './js/ds-uikit.esm.js').

Evento Cuándo
ds:open Antes de abrirse. Con preventDefault() se cancela.
ds:opened Al terminar de abrirse.
ds:close Antes de cerrarse. Cancelable.
ds:closed Al terminar de cerrarse.

event.detail.trigger es el botón que lo abrió o cerró, el fondo si fue por un clic ahí, o null si fue con Escape o la API. Más detalles en JavaScript.

  • aria-labelledby apunta al id del título. Si no hay título visible, usa aria-label.
  • Mientras está abierto, Tab y Shift+Tab no salen del modal y la página de fondo no hace scroll. aria-modal="true" hace que los lectores de pantalla ignoren el resto de la página.
  • Escape cierra solo el último elemento abierto (por ejemplo, un dropdown dentro del modal antes que el modal).
  • La X lleva type="button" y aria-label="Cerrar".
  • Si el modal confirma una acción que no se puede deshacer, pon autofocus en el botón seguro (Cancelar).
  • Con «reducir movimiento» activado, abre y cierra sin transición.
Clase Uso
modal Base: cubre la pantalla. Lleva hidden mientras está cerrado.
modal--sm / modal--lg Ventana de 320 px / 800 px.
modal--centered Centra la ventana verticalmente.
modal__backdrop Fondo oscuro. Un clic cierra el modal.
modal__dialog Ventana. Puede ser un <form>.
modal__header Cabecera con título y botón para cerrar.
modal__title Título (<h2> u otro nivel según la página).
modal__close Botón para cerrar, con data-ds-dismiss="modal".
modal__body Contenido. Hace scroll si no cabe.
modal__footer Acciones, alineadas a la derecha.
VariableValor por defectoUso
--ds-modal-width32remAncho máximo de la ventana.
--ds-modal-marginclamp(1rem, 5vw, 2rem)Separación mínima entre la ventana y el borde de la pantalla.
--ds-modal-bgvar(--ds-color-surface)Fondo de la ventana.
--ds-modal-colorvar(--ds-color-text)Color del texto.
--ds-modal-border-colortransparentColor del borde de la ventana.
--ds-modal-radiusvar(--ds-radius-lg)
--ds-modal-shadowvar(--ds-shadow-lg)
--ds-modal-padding-y1remEspacio vertical interno de cabecera, cuerpo y pie.
--ds-modal-padding-x1.25remEspacio horizontal interno de cabecera, cuerpo y pie.
--ds-modal-divider-colorvar(--ds-color-border)Línea bajo la cabecera y sobre el pie.
--ds-modal-title-font-sizevar(--ds-font-size-lg)Tamaño del título (modal__title).
--ds-modal-close-size2remTamaño del botón para cerrar (modal__close).
--ds-modal-backdrop-bgrgb(0 0 0 / 0.6)Color del fondo oscuro (modal__backdrop).
--ds-modal-backdrop-blur4pxDesenfoque de la página detrás del fondo.
--ds-modal-offset-0.75remDesplazamiento vertical de la ventana al abrir y cerrar.
--ds-modal-transition-durationvar(--ds-duration-fast)Duración del fundido al abrir y cerrar.
--ds-modal-z-indexvar(--ds-z-modal)Capa del modal (z-index).