Gaveta
Painel que entra por uma borda e ocupa o eixo inteiro dela: filtros de uma lista, menu no celular, carrinho,
detalhe de um registro. Divide com o modal a mecânica do <dialog> — o que muda é
a geometria e o movimento: no centro o diálogo cresce, na borda ele desliza, porque é assim que a gaveta diz de onde veio.
Exemplos
Todos abaixo são o mesmo Tucano.drawer() com opções diferentes.
Os quatro lados
side escolhe a borda; right é o padrão.
Largura da coluna
size nas laterais: 18, 24 ou 34rem.
Tons
tone muda o brilho, que nasce da borda em que a gaveta encosta.
Sem fechar por fora
closeOnBackdrop: false e closable: false, como no modal.
Conteúdo livre
content(nó) preenche o corpo, que rola quando o conteúdo passa da altura.
Menu de aplicação
Gaveta pela esquerda com a lista de navegação dentro — o exemplo completo está no menu lateral.
Como usar
Os mesmos dois caminhos do modal. Pelo JavaScript a gaveta é montada na hora e sai do DOM ao fechar; pelo template ela já está na página, e o componente só abre, fecha e anima.
Em JavaScript
Tucano.drawer({
title: 'Filtros',
text: 'Refine a lista de contratos.',
side: 'right', // left | right | top | bottom
size: 'md', // sm | md | lg — nas laterais, largura da coluna
tone: 'default', // default | danger | success | warning
actions: [
{ text: 'Limpar', variant: 'ghost' },
{ text: 'Aplicar', variant: 'primary', onClick: (drawer) => apply() },
],
}).content(form);
// Montada antes, aberta depois
const drawer = new Tucano.Drawer({ title: 'Carrinho', side: 'right' });
drawer.content(list).open();
drawer.close();actions segue as mesmas regras do modal: text, variant (padrão outline),
onClick, que recebe a instância, e closes: false para manter aberta. onClose(reason, drawer)
recebe 'button', 'action', 'escape', 'backdrop' ou 'api'.
No template do Django
O caso típico é o formulário de filtros de uma lista: ele é renderizado pelo servidor, com os valores que vieram no
request.GET, e a gaveta só o esconde até alguém pedir.
<dialog class="tuc-drawer is-right is-md" id="filters" aria-labelledby="filters-title">
<form class="tuc-drawer__panel" method="get">
<div class="tuc-drawer__top">
<div class="tuc-drawer__header">
<h2 class="tuc-drawer__title" id="filters-title">Filtros</h2>
</div>
<button type="button" class="tuc-btn is-ghost is-icon is-sm tuc-drawer__close" aria-label="Fechar" data-tuc-drawer-close>…</button>
</div>
<div class="tuc-drawer__body">
{{ form.as_div }}
</div>
<div class="tuc-drawer__footer">
<a class="tuc-btn is-ghost" href="?">Limpar</a>
<button class="tuc-btn is-primary">Aplicar</button>
</div>
</form>
</dialog>
<button class="tuc-btn is-outline" data-tuc-drawer="#filters">Filtros</button>| Atributo | Onde | Para quê |
|---|---|---|
data-tuc-drawer="#id" | Gatilho | Abre a gaveta do seletor ao clicar |
data-tuc-drawer-close | Botão dentro da gaveta | 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 |
Todo dialog.tuc-drawer é adotado no carregamento e a cada htmx:afterSwap. As classes internas
seguem o nome do componente: __panel, __top, __header, __title,
__text, __close, __body e __footer. No template, escreva o X e o
aria-labelledby: pelo JavaScript os dois saem prontos, à mão não.
Lados e tamanhos
A caixa estica no eixo da borda em que encosta — nas laterais ocupa a altura toda, em cima e embaixo a largura toda — e a borda da caixa fica só do lado que toca a página.
side | Classe | Entra de | O que size controla |
|---|---|---|---|
right | is-right | Direita — o padrão | Largura da coluna |
left | is-left | Esquerda | Largura da coluna |
top | is-top | Cima | Nada: a largura é a da tela e a altura, a do conteúdo |
bottom | is-bottom | Baixo | Nada: a largura é a da tela e a altura, a do conteúdo |
size | Largura nas laterais |
|---|---|
sm | 18rem |
md | 24rem — o padrão |
lg | 34rem |
Gaveta lateral é uma coluna, não um cartão, e por isso a escala é outra que a do modal. No celular (abaixo de 40rem)
ela para em min(20rem, 85vw) qualquer que seja o tamanho: coluna estreita ali não se lê, e a faixa que sobra
mostra que a página continua atrás. Os botões do rodapé ocupam a linha, como no modal.
O brilho nasce da borda
No modal a luz vem do centro da tela. Na gaveta ela sai do lado em que o painel encosta, e a elipse é maior: nascendo na borda, só metade dela fica na tela. Centrada, a luz apontaria para o meio da página enquanto o painel entra pela lateral.
Teclado e acessibilidade
É um <dialog> aberto com showModal(), igual ao modal: a página atrás fica inerte, o
foco não escapa e volta para quem abriu ao fechar.
| Tecla | Ação |
|---|---|
Tab Shift+Tab | Anda entre os controles da gaveta, 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 |
Divide o motor com o modal
Top layer, foco preso, Esc e foco devolvido moram numa base só, e não em duas implementações parecidas. É isso que impede uma correção feita num deles de deixar de valer no outro.
Com title, a gaveta ganha aria-labelledby. O X é um .tuc-btn com
aria-label="Fechar". Com prefers-reduced-motion, a entrada dura 100 ms e não desliza.
API
Gerada do código a cada build — se algo não está aqui, não existe.
dialog.tuc-drawer [data-tuc-drawer-close] [data-tuc-drawer]new Tucano.Drawer(opcoes) Tucano.drawer()data-backdrop data-closable data-tuc-draweropen close contentOpções
| Opção | Padrão | Para quê |
|---|---|---|
title | null | |
text | '' | |
side | 'right' | left | right | top | bottom |
size | 'md' | sm | md | lg — nas laterais, largura da coluna |
tone | 'default' | default | danger | success | warning |
closable | true | |
closeOnBackdrop | true | |
actions | null | [{ text, variant, onClick, closes }] — closes:false mantem aberto |
onClose | null | |
className | '' |