Modal

Un modal es un elemento popover. Cualquier botón cuyo popovertarget coincida con el id del modal lo abre — sin script por ningún lado, incluidos el fondo, el cierre al pulsar fuera, Escape y el manejo del foco, de todo lo cual se ocupa el navegador.

Por eso el id es obligatorio y el componente lanza un error sin él: el id es todo el cableado.

Todos los modales de esta página se abren de verdad: pruébalos.

fragment(
  button({ popovertarget: 'demo-basic' }, 'Abrir modal'),
  modal({ id: 'demo-basic', title: '¿Recompilar el sitio?' },
    'Esto ejecuta sitelo build y vuelve a publicar dist/.',
  ),
)

Con pie

Un botón de cerrar es cualquier botón que apunte al mismo id con popovertargetaction="hide".

fragment(
  button({ color: 'danger', popovertarget: 'demo-confirm' }, 'Eliminar página…'),
  modal({
    id: 'demo-confirm',
    title: '¿Eliminar esta página?',
    footer: stack({ direction: 'row', gap: 'sm' },
      button({
        variant: 'ghost',
        color: 'neutral',
        popovertarget: 'demo-confirm',
        popovertargetaction: 'hide',
      }, 'Cancelar'),
      button({ color: 'danger' }, 'Eliminar'),
    ),
  }, 'Esto no se puede deshacer. El HTML generado desaparece en la siguiente compilación.'),
)

Tamaños

fragment(
  stack({ direction: 'row', gap: 'sm', wrap: true },
    button({ variant: 'outline', color: 'neutral', popovertarget: 'demo-sm' }, 'Pequeño'),
    button({ variant: 'outline', color: 'neutral', popovertarget: 'demo-md' }, 'Mediano'),
    button({ variant: 'outline', color: 'neutral', popovertarget: 'demo-lg' }, 'Grande'),
  ),
  modal({ id: 'demo-sm', size: 'sm', title: 'Pequeño' }, 'size: sm — unas 24rem.'),
  modal({ id: 'demo-md', title: 'Mediano' }, 'El valor por defecto — unas 32rem.'),
  modal({ id: 'demo-lg', size: 'lg', title: 'Grande' }, 'size: lg — unas 48rem.'),
)

Formularios dentro de un modal

fragment(
  button({ variant: 'soft', popovertarget: 'demo-form' }, 'Página nueva…'),
  modal({
    id: 'demo-form',
    title: 'Página nueva',
    footer: stack({ direction: 'row', gap: 'sm' },
      button({ variant: 'ghost', color: 'neutral', popovertarget: 'demo-form', popovertargetaction: 'hide' }, 'Cancelar'),
      button({ type: 'submit' }, 'Crear'),
    ),
  },
    stack({ gap: 'md' },
      textField({ label: 'Título', name: 'modal-title', placeholder: 'Acerca de' }),
      selectField({ label: 'Extensión', name: 'modal-ext', options: ['.ht.js', '.ht.ts', '.ht.jsx'] }),
    ),
  ),
)

Sin botón de cerrar

closable: false quita la × de la esquina. Escape y el clic fuera lo siguen cerrando: un popover no se puede volver realmente bloqueante, y de todos modos ese suele ser el comportamiento correcto.

fragment(
  button({ variant: 'outline', color: 'neutral', popovertarget: 'demo-bare' }, 'Sin botón de cerrar'),
  modal({ id: 'demo-bare', title: 'Pulsa Escape', closable: false },
    'O haz clic en cualquier sitio fuera de este diálogo.',
  ),
)

Contenido largo

El cuerpo se desplaza; la cabecera y el pie se quedan quietos.

fragment(
  button({ variant: 'outline', color: 'neutral', popovertarget: 'demo-long' }, 'Modal largo'),
  modal({
    id: 'demo-long',
    title: 'Notas de la versión',
    footer: button({ popovertarget: 'demo-long', popovertargetaction: 'hide' }, 'Cerrar'),
  },
    stack({ gap: 'md' },
      ...Array.from({ length: 12 }, (unused, index) =>
        text({ variant: 'small', tone: 'muted' }, 'Cambio ' + (index + 1) + ' — se arregló algo.'),
      ),
    ),
  ),
)

Desplazamiento del fondo

La página que hay detrás de un modal abierto no se desplaza. Eso es lo único que la API de popover te deja a ti, y aquí está hecho en CSS: sin script y sin nada que inicializar. Pasa lockScroll: false para dejar que el fondo se desplace como siempre.

Soporte de navegadores

La API de popover está disponible en todos los navegadores actuales. En uno demasiado viejo como para conocerla, el modal se dibuja en línea dentro de la página en vez de encima: visible y utilizable, solo que sin superponerse. No desaparece nada.

Props

PropTipoPor defectoDescripción
idstring—Obligatorio. Aquello a lo que apunta el popovertarget de un disparador.
titleChild—Encabezado, y nombre accesible del diálogo.
size'sm' | 'md' | 'lg''md'Ancho máximo.
footerChild—Fila inferior, sobre su propia banda teñida.
closablebooleantrueMostrar la × en la cabecera.
closeLabelstring'Close'Nombre accesible de ese botón.
lockScrollbooleantrueImpedir que la página de detrás se desplace mientras está abierto.

closeButton({ target }) dibuja esa × por su cuenta, para una cabecera que construyas tú.