Tucano v0.37.2

Modal

Diálogo que se superpone a todo, con el fondo desenfocado y un brillo del color del tono. Hecho sobre el <dialog> nativo: de él vienen la capa por encima de cualquier z-index, el foco atrapado, el Esc y el foco devuelto a quien lo abrió — el componente solo dibuja la caja y la anima.

Este <dialog> ya estaba en la página. Es el camino para un formulario renderizado por el servidor.

Ejemplos

Todos los de abajo son el mismo Tucano.modal() con opciones distintas.

Tamaños

size define el ancho máximo de la caja; md es el predeterminado.

Tonos

tone cambia el brillo del fondo y el aura de la caja.

Confirmación con promesa

Tucano.confirm() resuelve true o false; cerrar desde fuera es un rechazo.

Hoja en el móvil

sheet: true sube desde abajo en pantallas estrechas. Estrecha la ventana para verlo.

Sin cerrar desde fuera

closeOnBackdrop: false ignora el clic en el fondo; closable: false quita la X y el Esc.

Motivo del cierre

onClose recibe por dónde salió la persona. Ciérralo de formas distintas.

Contenido libre

content(nodo) pone cualquier elemento en el cuerpo, que se desplaza cuando supera la altura.

Acción que no cierra

closes: false en una acción mantiene el modal abierto después del clic.

Cómo usar

Hay dos caminos. Desde JavaScript, el modal se monta en el momento, entra en el <body> al abrirse y sale al cerrarse. Desde la plantilla, el <dialog> ya está en la página y el componente solo lo abre, lo cierra y lo anima.

En JavaScript

Tucano.modal({
  title: 'Eliminar contrato',
  text: 'Esta acción no se puede deshacer.',
  tone: 'danger',     // default | danger | success | warning
  size: 'md',         // sm | md | lg | full
  sheet: true,        // en el móvil sube desde abajo
  actions: [
    { text: 'Cancelar', variant: 'outline' },
    { text: 'Eliminar', variant: 'danger', onClick: () => deleteContract() },
  ],
  onClose: (reason, modal) => console.log(reason),
});

// Solo con texto, el atajo acepta una cadena
Tucano.modal('Archivo enviado.');

El atajo Tucano.modal() crea y abre en un paso, y devuelve la instancia. Para montarlo antes y abrirlo después, usa la clase:

const m = new Tucano.Modal({ title: 'Nuevo contacto', size: 'lg' });
m.content(form);         // cualquier nodo, o un array de nodos
m.open();
m.close();               // anima la salida y llama a onClose(reason)

Confirmación

El caso más común de modal en un CRUD es preguntar antes de borrar. Tucano.confirm() devuelve una promesa, y el código sigue leyéndose de arriba abajo:

if (await Tucano.confirm({ title: '¿Eliminar contrato?', text: 'Esta acción no se puede deshacer.', confirm: 'Eliminar' })) {
  deleteContract();
}
OpciónPredeterminadoPara qué
confirm'Confirmar'Texto del botón que resuelve true
cancel'Cancelar'Texto del botón que resuelve false
tone'danger'Con danger el botón de confirmar es rojo; con cualquier otro tono, primario

Las demás opciones del modal también valen aquí, excepto actions, que confirm() monta por su cuenta. Un onClose que pases se sigue llamando.

Cerrar desde fuera es un rechazo, no un limbo

Con la X, con Esc o con el fondo, la promesa resuelve false. Sin eso quedaría pendiente para siempre, y el await nunca volvería.

Acciones

Cada elemento de actions se convierte en un .tuc-btn en el pie, en el orden en que se escribió.

ClavePredeterminadoPara qué
text—Texto del botón
variant'outline'Variante del botón: primary, outline, ghost, danger…
onClick—Recibe la instancia del modal
closestruefalse mantiene el modal abierto después del clic

Escrito en la plantilla

Cuando el contenido viene renderizado por el servidor — un formulario de Django, con errores y csrf_token —, el <dialog> vive en la plantilla. Todo dialog.tuc-modal se adopta al cargar y en cada htmx:afterSwap; abrir no lo inserta y cerrar no lo quita, porque el nodo es de quien escribió el HTML.

<dialog class="tuc-modal is-md" id="delete" aria-labelledby="delete-title">
  <form class="tuc-modal__panel" method="post" action="{% url 'contract-delete' contract.pk %}">
    {% csrf_token %}
    <div class="tuc-modal__top">
      <div class="tuc-modal__header">
        <h2 class="tuc-modal__title" id="delete-title">¿Eliminar contrato?</h2>
        <p class="tuc-modal__text">Esta acción no se puede deshacer.</p>
      </div>
      <button type="button" class="tuc-btn is-ghost is-icon is-sm tuc-modal__close" aria-label="Cerrar" data-tuc-modal-close>…</button>
    </div>
    <div class="tuc-modal__footer">
      <button type="button" class="tuc-btn is-outline" data-tuc-modal-close>Cancelar</button>
      <button class="tuc-btn is-danger">Eliminar</button>
    </div>
  </form>
</dialog>

<button class="tuc-btn is-outline" data-tuc-modal="#delete">Eliminar</button>
AtributoDóndePara qué
data-tuc-modal="#id"DisparadorAbre el <dialog> del selector al hacer clic
data-tuc-modal-closeBotón dentro del diálogoCierra, con el motivo 'button'
data-closable="false"<dialog>Esc deja de cerrar
data-backdrop="false"<dialog>El clic en el fondo deja de cerrar

Las clases son las mismas que monta el JavaScript: __panel es la caja, __top contiene el __header (con __title y __text) y el __close, __body es el cuerpo que se desplaza y __footer alinea las acciones a la derecha. El tamaño y el tono van como clase en el propio <dialog>: is-lg, is-danger, is-sheet.

En la plantilla, la X y el nombre del diálogo son cosa tuya

Desde JavaScript el botón de cerrar y el aria-labelledby salen listos. En el <dialog> escrito a mano, escribe los dos: sin la X la persona depende del Esc, y sin el aria-labelledby el lector de pantalla anuncia solo "diálogo".

Confirmar antes de enviar un formulario

Para un formulario de eliminación que ya existe en la página, confirm() intercepta el envío y lo deja pasar solo con un sí:

document.querySelector('#delete-contract').addEventListener('submit', async (e) => {
  e.preventDefault();
  const ok = await Tucano.confirm({ title: '¿Eliminar contrato?', confirm: 'Eliminar' });
  if (ok) e.target.submit();
});

El <dialog> cerrado tiene que volver a display: none — y la biblioteca ya lo hace

La regla del modal declara display: grid, que gana a la hoja del navegador. Sin .tuc-modal:not([open]) { display: none }, un <dialog> quieto en la plantilla quedaría renderizado, fijo y transparente sobre toda la página: no se ve nada, y ningún clic funciona. Por eso no necesita hidden.

Tonos y brillo

El fondo no es solo oscuro: un brillo radial detrás de la caja separa el diálogo de lo que quedó debajo, y el desenfoque saca la página del foco. El tono cambia el color de ese brillo y del aura alrededor de la caja — no el color de los botones, que sigue siendo elección de cada acción.

toneClaseColor del brillo
defaultis-default--tuc-accent — cambiar el acento cambia el brillo
dangeris-danger--tuc-danger-fill
successis-success--tuc-success
warningis-warning--tuc-warning

Tamaños

La caja ocupa el ancho disponible hasta el límite del tamaño, y el cuerpo se desplaza cuando el contenido supera la altura de la pantalla.

sizeAncho máximoPara qué
sm22remConfirmación corta
md30remEl predeterminado: aviso, formulario de pocos campos
lg44remTabla o formulario ancho
fullla pantalla, con un margen de 1remAncho y alto completos: editor, vista previa de documento

Por debajo de 40rem los botones del pie ocupan la fila, para que el área táctil crezca. Con sheet, la caja pierde las esquinas de abajo, se apoya en el borde inferior y respeta el área segura del iPhone. Es opcional porque no todo modal quiere convertirse en panel lateral en el móvil.

Teclado y accesibilidad

showModal() pone el diálogo en la top layer y deja inerte el resto de la página: el foco no escapa detrás de él, y al cerrar vuelve a quien lo abrió. Nada de eso es JavaScript nuestro — es el elemento nativo.

TeclaAcción
Tab Shift+TabRecorre los controles del diálogo, sin volver a la página
Enter EspacioActiva el botón con foco
EscCierra con animación y devuelve el foco; no hace nada con closable: false

Por qué <dialog>, y no un div con z-index

La top layer queda por encima de cualquier z-index y es inmune a un ancestro con overflow: hidden o transform — los tres motivos por los que un modal artesanal aparece cortado o por debajo. El Esc nativo cierra al instante, sin animación; el componente intercepta el cancel para cerrar por el mismo camino que los demás, que anima e informa el motivo.

Con title, el diálogo recibe un aria-labelledby que apunta a él, y el lector de pantalla anuncia el título al abrirse. La X es un .tuc-btn con aria-label="Fechar". Con prefers-reduced-motion, la entrada dura 100 ms y no crece.

Motivo en onCloseCuándo
'button'La X, o un data-tuc-modal-close en la plantilla
'action'Un botón de actions
'escape'La tecla Esc
'backdrop'Clic fuera de la caja
'api'close() llamado sin argumento

El modal y el panel lateral comparten esta mecánica: una corrección hecha en uno vale para el otro.

API

Generada a partir del código en cada build — si algo no está aquí, no existe.

Marcado
dialog.tuc-modal [data-tuc-modal-close] [data-tuc-modal]
En JS
new Tucano.Modal(opcoes) Tucano.modal() Tucano.confirm()
Atributos
data-backdrop data-closable data-tuc-modal
Métodos
open close content

Opciones

Las notas de esta tabla salen de los comentarios del código fuente, que están escritos en portugués.

OpciónPor defectoPara qué
titlenull
text''
size'md'sm | md | lg | full
tone'default'default | danger | success | warning
sheetfalseno celular sobe do rodape em vez de surgir no centro
closabletruebotao X e Escape
closeOnBackdroptrue
actionsnull[{ text, variant, onClick, closes }] — closes:false mantem aberto
onClosenull
className''