Modal
.modal muestra una ventana sobre la página, con un fondo oscuro detrás. Necesita ds-uikit.js.
.modalllevahidden,role="dialog",aria-modal="true"yaria-labelledbycon el id del título.- Dentro van el fondo (
modal__backdrop) y la ventana (modal__dialog), conmodal__header(título y botón para cerrar),modal__bodyymodal__footer. - El botón que lo abre lleva
data-ds-toggle="modal"ydata-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>export default function ModalBasico() { return ( <> <button type="button" className="btn btn--primary" data-ds-toggle="modal" data-ds-target="#modal-basico">Abrir modal</button>
<div className="modal" id="modal-basico" role="dialog" aria-modal="true" aria-labelledby="modal-basico-titulo" hidden> <div className="modal__backdrop" /> <div className="modal__dialog"> <div className="modal__header"> <h2 className="modal__title" id="modal-basico-titulo">Guardar cambios</h2> <button type="button" className="modal__close" data-ds-dismiss="modal" aria-label="Cerrar" /> </div> <div className="modal__body"> <p>Tienes cambios sin guardar. ¿Quieres guardarlos antes de salir?</p> </div> <div className="modal__footer"> <button type="button" className="btn" data-ds-dismiss="modal">Cancelar</button> <button type="button" className="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.
Tamaños
Sección titulada «Tamaños»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>export default function ModalTamanos() { return ( <> <button type="button" className="btn btn--primary" data-ds-toggle="modal" data-ds-target="#modal-sm">Pequeño</button> <button type="button" className="btn btn--primary" data-ds-toggle="modal" data-ds-target="#modal-lg">Grande</button>
<div className="modal modal--sm" id="modal-sm" role="dialog" aria-modal="true" aria-labelledby="modal-sm-titulo" hidden> <div className="modal__backdrop" /> <div className="modal__dialog"> <div className="modal__header"> <h2 className="modal__title" id="modal-sm-titulo">Modal pequeño</h2> <button type="button" className="modal__close" data-ds-dismiss="modal" aria-label="Cerrar" /> </div> <div className="modal__body"> <p>320 px de ancho máximo.</p> </div> </div> </div>
<div className="modal modal--lg" id="modal-lg" role="dialog" aria-modal="true" aria-labelledby="modal-lg-titulo" hidden> <div className="modal__backdrop" /> <div className="modal__dialog"> <div className="modal__header"> <h2 className="modal__title" id="modal-lg-titulo">Modal grande</h2> <button type="button" className="modal__close" data-ds-dismiss="modal" aria-label="Cerrar" /> </div> <div className="modal__body"> <p>800 px de ancho máximo. En pantallas más angostas ocupa el ancho disponible.</p> </div> </div> </div> </> );}Centrado
Sección titulada «Centrado»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>export default function ModalCentrado() { return ( <> <button type="button" className="btn btn--primary" data-ds-toggle="modal" data-ds-target="#modal-centrado">Abrir modal centrado</button>
<div className="modal modal--centered" id="modal-centrado" role="dialog" aria-modal="true" aria-labelledby="modal-centrado-titulo" hidden> <div className="modal__backdrop" /> <div className="modal__dialog"> <div className="modal__header"> <h2 className="modal__title" id="modal-centrado-titulo">Sesión por vencer</h2> <button type="button" className="modal__close" data-ds-dismiss="modal" aria-label="Cerrar" /> </div> <div className="modal__body"> <p>Tu sesión vence en 5 minutos. ¿Quieres seguir conectado?</p> </div> <div className="modal__footer"> <button type="button" className="btn" data-ds-dismiss="modal">Cerrar sesión</button> <button type="button" className="btn btn--primary" data-ds-dismiss="modal">Seguir conectado</button> </div> </div> </div> </> );}Contenido largo
Sección titulada «Contenido largo»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>export default function ModalContenidoLargo() { return ( <> <button type="button" className="btn btn--primary" data-ds-toggle="modal" data-ds-target="#modal-largo">Ver términos</button>
<div className="modal" id="modal-largo" role="dialog" aria-modal="true" aria-labelledby="modal-largo-titulo" hidden> <div className="modal__backdrop" /> <div className="modal__dialog"> <div className="modal__header"> <h2 className="modal__title" id="modal-largo-titulo">Términos y condiciones</h2> <button type="button" className="modal__close" data-ds-dismiss="modal" aria-label="Cerrar" /> </div> <div className="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 className="modal__footer"> <button type="button" className="btn" data-ds-dismiss="modal">Rechazar</button> <button type="button" className="btn btn--primary" data-ds-dismiss="modal">Aceptar</button> </div> </div> </div> </> );}Formulario
Sección titulada «Formulario»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>export default function ModalFormulario() { return ( <> <button type="button" className="btn btn--primary" data-ds-toggle="modal" data-ds-target="#modal-contacto">Escríbenos</button>
<div className="modal" id="modal-contacto" role="dialog" aria-modal="true" aria-labelledby="modal-contacto-titulo" hidden> <div className="modal__backdrop" /> <form className="modal__dialog" action="/contacto" method="post"> <div className="modal__header"> <h2 className="modal__title" id="modal-contacto-titulo">Escríbenos</h2> <button type="button" className="modal__close" data-ds-dismiss="modal" aria-label="Cerrar" /> </div> <div className="modal__body"> <div className="form-field"> <label className="form-label" htmlFor="contacto-nombre">Nombre</label> <input className="form-input" id="contacto-nombre" name="nombre" type="text" autoComplete="name" data-autofocus /> </div> <div className="form-field"> <label className="form-label" htmlFor="contacto-correo">Correo</label> <input className="form-input" id="contacto-correo" name="correo" type="email" autoComplete="email" /> </div> <div className="form-field"> <label className="form-label" htmlFor="contacto-mensaje">Mensaje</label> <textarea className="form-input" id="contacto-mensaje" name="mensaje" rows="3" /> </div> </div> <div className="modal__footer"> <button type="button" className="btn" data-ds-dismiss="modal">Cancelar</button> <button type="submit" className="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.
Fondo estático
Sección titulada «Fondo estático»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>export default function ModalFondoEstatico() { return ( <> <button type="button" className="btn btn--primary" data-ds-toggle="modal" data-ds-target="#modal-estatico">Abrir modal</button>
<div className="modal" id="modal-estatico" role="dialog" aria-modal="true" aria-labelledby="modal-estatico-titulo" data-ds-backdrop="static" hidden> <div className="modal__backdrop" /> <div className="modal__dialog"> <div className="modal__header"> <h2 className="modal__title" id="modal-estatico-titulo">Elige una opción</h2> <button type="button" className="modal__close" data-ds-dismiss="modal" aria-label="Cerrar" /> </div> <div className="modal__body"> <p>Un clic en el fondo no cierra este modal. Usa los botones o Escape.</p> </div> <div className="modal__footer"> <button type="button" className="btn" data-ds-dismiss="modal">Cancelar</button> <button type="button" className="btn btn--primary" data-ds-dismiss="modal">Continuar</button> </div> </div> </div> </> );}JavaScript
Sección titulada «JavaScript»- Al abrir: bloquea el scroll de la página, atrapa el foco dentro del modal y enfoca el elemento con
autofocus(odata-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 condata-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 selectorconfirmar.open();confirmar.close();confirmar.toggle();confirmar.isOpen; // true o false
// Pedir confirmación antes de cerrar un formulario con cambiosdocument.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.
Accesibilidad
Sección titulada «Accesibilidad»aria-labelledbyapunta al id del título. Si no hay título visible, usaaria-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"yaria-label="Cerrar". - Si el modal confirma una acción que no se puede deshacer, pon
autofocusen 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. |
Variables
Sección titulada «Variables»| Variable | Valor por defecto | Uso |
|---|---|---|
--ds-modal-width | 32rem | Ancho máximo de la ventana. |
--ds-modal-margin | clamp(1rem, 5vw, 2rem) | Separación mínima entre la ventana y el borde de la pantalla. |
--ds-modal-bg | var(--ds-color-surface) | Fondo de la ventana. |
--ds-modal-color | var(--ds-color-text) | Color del texto. |
--ds-modal-border-color | transparent | Color del borde de la ventana. |
--ds-modal-radius | var(--ds-radius-lg) | |
--ds-modal-shadow | var(--ds-shadow-lg) | |
--ds-modal-padding-y | 1rem | Espacio vertical interno de cabecera, cuerpo y pie. |
--ds-modal-padding-x | 1.25rem | Espacio horizontal interno de cabecera, cuerpo y pie. |
--ds-modal-divider-color | var(--ds-color-border) | Línea bajo la cabecera y sobre el pie. |
--ds-modal-title-font-size | var(--ds-font-size-lg) | Tamaño del título (modal__title). |
--ds-modal-close-size | 2rem | Tamaño del botón para cerrar (modal__close). |
--ds-modal-backdrop-bg | rgb(0 0 0 / 0.6) | Color del fondo oscuro (modal__backdrop). |
--ds-modal-backdrop-blur | 4px | Desenfoque de la página detrás del fondo. |
--ds-modal-offset | -0.75rem | Desplazamiento vertical de la ventana al abrir y cerrar. |
--ds-modal-transition-duration | var(--ds-duration-fast) | Duración del fundido al abrir y cerrar. |
--ds-modal-z-index | var(--ds-z-modal) | Capa del modal (z-index). |