Modal
Diálogo que se sobrepõe a tudo, com o fundo desfocado e um brilho da cor do tom. Feito sobre o
<dialog> nativo: é dele que vêm a camada acima de qualquer z-index, o foco
preso, o Esc e o foco devolvido a quem abriu — o componente só desenha a caixa e anima.
Exemplos
Todos abaixo são o mesmo Tucano.modal() com opções diferentes.
Tamanhos
size define a largura máxima da caixa; md é o padrão.
Tons
tone muda o brilho do fundo e a aura da caixa.
Confirmação com promessa
Tucano.confirm() resolve true ou false; fechar por fora é recusa.
Folha no celular
sheet: true sobe do rodapé em telas estreitas. Estreite a janela para ver.
Sem fechar por fora
closeOnBackdrop: false ignora o clique no fundo; closable: false tira o X e o Esc.
Motivo do fechamento
onClose recebe por onde a pessoa saiu. Feche de jeitos diferentes.
Conteúdo livre
content(nó) põe qualquer elemento no corpo, que rola quando passa da altura.
Ação que não fecha
closes: false numa ação mantém o modal aberto depois do clique.
Como usar
Há dois caminhos. Pelo JavaScript, o modal é montado na hora, entra no <body> ao abrir e sai
ao fechar. Pelo template, o <dialog> já está na página e o componente só abre, fecha e anima.
Em JavaScript
Tucano.modal({
title: 'Excluir contrato',
text: 'Esta ação não pode ser desfeita.',
tone: 'danger', // default | danger | success | warning
size: 'md', // sm | md | lg | full
sheet: true, // no celular sobe do rodapé
actions: [
{ text: 'Cancelar', variant: 'outline' },
{ text: 'Excluir', variant: 'danger', onClick: () => deleteContract() },
],
onClose: (reason, modal) => console.log(reason),
});
// Só com texto, o atalho aceita uma string
Tucano.modal('Arquivo enviado.');O atalho Tucano.modal() cria e abre num passo, e devolve a instância. Para montar antes e abrir depois,
use a classe:
const m = new Tucano.Modal({ title: 'Novo contato', size: 'lg' });
m.content(form); // qualquer nó, ou um array de nós
m.open();
m.close(); // anima a saída e chama onClose(reason)Confirmação
O caso mais comum de modal num CRUD é perguntar antes de apagar. Tucano.confirm() devolve uma promessa, e o
código continua lendo de cima para baixo:
if (await Tucano.confirm({ title: 'Excluir contrato?', text: 'Esta ação não pode ser desfeita.', confirm: 'Excluir' })) {
deleteContract();
}| Opção | Padrão | Para quê |
|---|---|---|
confirm | 'Confirmar' | Rótulo do botão que resolve true |
cancel | 'Cancelar' | Rótulo do botão que resolve false |
tone | 'danger' | Com danger o botão de confirmar é vermelho; com qualquer outro tom, primário |
As demais opções do modal valem aqui também, menos actions, que o confirm() monta sozinho. Um
onClose passado continua sendo chamado.
Fechar por fora é recusa, não limbo
Pelo X, pelo Esc ou pelo fundo, a promessa resolve false. Sem isso ela ficaria pendente para sempre, e o await nunca voltaria.
Ações
Cada item de actions vira um .tuc-btn no rodapé, na ordem em que foi escrito.
| Chave | Padrão | Para quê |
|---|---|---|
text | — | Rótulo do botão |
variant | 'outline' | Variante do botão: primary, outline, ghost, danger… |
onClick | — | Recebe a instância do modal |
closes | true | false mantém o modal aberto depois do clique |
Escrito no template
Quando o conteúdo vem renderizado pelo servidor — um formulário do Django, com erros e csrf_token —,
o <dialog> mora no template. Todo dialog.tuc-modal é adotado no carregamento e a cada
htmx:afterSwap; abrir não o insere e fechar não o remove, porque o nó é de quem escreveu o 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">Excluir contrato?</h2>
<p class="tuc-modal__text">Esta ação não pode ser desfeita.</p>
</div>
<button type="button" class="tuc-btn is-ghost is-icon is-sm tuc-modal__close" aria-label="Fechar" 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">Excluir</button>
</div>
</form>
</dialog>
<button class="tuc-btn is-outline" data-tuc-modal="#delete">Excluir</button>| Atributo | Onde | Para quê |
|---|---|---|
data-tuc-modal="#id" | Gatilho | Abre o <dialog> do seletor ao clicar |
data-tuc-modal-close | Botão dentro do diálogo | Fecha, com o motivo 'button' |
data-closable="false" | <dialog> | O Esc deixa de fechar |
data-backdrop="false" | <dialog> | O clique no fundo deixa de fechar |
As classes são as mesmas que o JavaScript monta: __panel é a caixa, __top guarda o
__header (com __title e __text) e o __close, __body é o
corpo que rola e __footer alinha as ações à direita. Tamanho e tom vão como classe no próprio
<dialog>: is-lg, is-danger, is-sheet.
No template, o X e o nome do diálogo são seus
Pelo JavaScript o botão de fechar e o aria-labelledby saem prontos. No <dialog> escrito à mão, escreva os dois: sem o X a pessoa depende do Esc, e sem o aria-labelledby o leitor de tela anuncia só "diálogo".
Confirmar antes de enviar um formulário
Para um formulário de exclusão que já existe na página, o confirm() intercepta o envio e deixa passar só com o sim:
document.querySelector('#delete-contract').addEventListener('submit', async (e) => {
e.preventDefault();
const ok = await Tucano.confirm({ title: 'Excluir contrato?', confirm: 'Excluir' });
if (ok) e.target.submit();
});O <dialog> fechado precisa voltar a display: none — e a biblioteca já faz isso
A regra do modal declara display: grid, que vence a folha do navegador. Sem .tuc-modal:not([open]) { display: none }, um <dialog> parado no template ficaria renderizado, fixo e transparente sobre a página inteira: nada aparece, e nenhum clique funciona. Por isso ele não precisa de hidden.
Tons e brilho
O fundo não é só escuro: um brilho radial atrás da caixa separa o diálogo do que ficou embaixo, e o desfoque tira a página do foco. O tom troca a cor desse brilho e da aura em volta da caixa — não a cor dos botões, que continua sendo escolha de cada ação.
tone | Classe | Cor do brilho |
|---|---|---|
default | is-default | --tuc-accent — trocar o destaque troca o brilho |
danger | is-danger | --tuc-danger-fill |
success | is-success | --tuc-success |
warning | is-warning | --tuc-warning |
Tamanhos
A caixa ocupa a largura disponível até o limite do tamanho, e o corpo rola quando o conteúdo passa da altura da tela.
size | Largura máxima | Para quê |
|---|---|---|
sm | 22rem | Confirmação curta |
md | 30rem | O padrão: aviso, formulário de poucos campos |
lg | 44rem | Tabela ou formulário largo |
full | a tela, com folga de 1rem | Largura e altura inteiras: editor, visualização de documento |
Abaixo de 40rem os botões do rodapé ocupam a linha, para o alvo de toque crescer. Com sheet, a caixa
perde as bordas de baixo, encosta no rodapé e respeita a área segura do iPhone. É opcional porque nem todo modal quer
virar gaveta no celular.
Teclado e acessibilidade
O showModal() põe o diálogo na top layer e deixa o resto da página inerte: o foco não escapa
para trás dele, e ao fechar volta para quem abriu. Nada disso é JavaScript nosso — é o elemento nativo.
| Tecla | Ação |
|---|---|
Tab Shift+Tab | Anda entre os controles do diálogo, sem voltar para a página |
Enter Espaço | Aciona o botão focado |
Esc | Fecha com animação e devolve o foco; não faz nada com closable: false |
Por que <dialog>, e não uma div com z-index
A top layer fica acima de qualquer z-index e é imune a ancestral com overflow: hidden ou transform — os três motivos de um modal artesanal aparecer cortado ou por baixo. O Esc nativo fecha na hora, sem animação; o componente intercepta o cancel para fechar pelo mesmo caminho dos outros, que anima e informa o motivo.
Com title, o diálogo ganha aria-labelledby apontando para ele, e o leitor de tela anuncia o título ao
abrir. O X é um .tuc-btn com aria-label="Fechar". Com prefers-reduced-motion, a entrada
dura 100 ms e não cresce.
Motivo em onClose | Quando |
|---|---|
'button' | O X, ou um data-tuc-modal-close no template |
'action' | Um botão de actions |
'escape' | A tecla Esc |
'backdrop' | Clique fora da caixa |
'api' | close() chamado sem argumento |
O modal e a gaveta dividem essa mecânica: uma correção feita num deles vale para o outro.
API
Gerada do código a cada build — se algo não está aqui, não existe.
dialog.tuc-modal [data-tuc-modal-close] [data-tuc-modal]new Tucano.Modal(opcoes) Tucano.modal() Tucano.confirm()data-backdrop data-closable data-tuc-modalopen close contentOpções
| Opção | Padrão | Para quê |
|---|---|---|
title | null | |
text | '' | |
size | 'md' | sm | md | lg | full |
tone | 'default' | default | danger | success | warning |
sheet | false | no celular sobe do rodape em vez de surgir no centro |
closable | true | botao X e Escape |
closeOnBackdrop | true | |
actions | null | [{ text, variant, onClick, closes }] — closes:false mantem aberto |
onClose | null | |
className | '' |