Panel lateral
Panel que entra por un borde y ocupa todo su eje: filtros de una lista, menú en el móvil, carrito,
detalle de un registro. Comparte con el modal la mecánica del <dialog> — lo que cambia es
la geometría y el movimiento: en el centro el diálogo crece, en el borde se desliza, porque así el panel dice de dónde vino.
Ejemplos
Todos los de abajo son el mismo Tucano.drawer() con opciones distintas.
Los cuatro lados
side elige el borde; right es el predeterminado.
Ancho de la columna
size en los laterales: 18, 24 o 34rem.
Tonos
tone cambia el brillo, que nace del borde en el que se apoya el panel.
Sin cerrar desde fuera
closeOnBackdrop: false y closable: false, como en el modal.
Contenido libre
content(nodo) rellena el cuerpo, que se desplaza cuando el contenido supera la altura.
Menú de aplicación
Panel por la izquierda con la lista de navegación dentro — el ejemplo completo está en el menú lateral.
Cómo usar
Los mismos dos caminos del modal. Desde JavaScript el panel se monta en el momento y sale del DOM al cerrarse; desde la plantilla ya está en la página, y el componente solo lo abre, lo cierra y lo anima.
En JavaScript
Tucano.drawer({
title: 'Filtros',
text: 'Afina la lista de contratos.',
side: 'right', // left | right | top | bottom
size: 'md', // sm | md | lg — en los laterales, ancho de la columna
tone: 'default', // default | danger | success | warning
actions: [
{ text: 'Limpiar', variant: 'ghost' },
{ text: 'Aplicar', variant: 'primary', onClick: (drawer) => apply() },
],
}).content(form);
// Montado antes, abierto después
const drawer = new Tucano.Drawer({ title: 'Carrito', side: 'right' });
drawer.content(list).open();
drawer.close();actions sigue las mismas reglas del modal: text, variant (predeterminado outline),
onClick, que recibe la instancia, y closes: false para mantenerlo abierto. onClose(reason, drawer)
recibe 'button', 'action', 'escape', 'backdrop' o 'api'.
En la plantilla de Django
El caso típico es el formulario de filtros de una lista: lo renderiza el servidor, con los valores que llegaron en
request.GET, y el panel solo lo oculta hasta que alguien lo pida.
<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="Cerrar" 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="?">Limpiar</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 | Dónde | Para qué |
|---|---|---|
data-tuc-drawer="#id" | Disparador | Abre el panel del selector al hacer clic |
data-tuc-drawer-close | Botón dentro del panel | Cierra, con el motivo 'button' |
data-closable="false" | <dialog> | Esc deja de cerrar |
data-backdrop="false" | <dialog> | El clic en el fondo deja de cerrar |
Todo dialog.tuc-drawer se adopta al cargar y en cada htmx:afterSwap. Las clases internas
siguen el nombre del componente: __panel, __top, __header, __title,
__text, __close, __body y __footer. En la plantilla, escribe la X y el
aria-labelledby: desde JavaScript los dos salen listos, a mano no.
Lados y tamaños
La caja se estira en el eje del borde en el que se apoya — en los laterales ocupa toda la altura, arriba y abajo todo el ancho — y el borde de la caja queda solo del lado que toca la página.
side | Clase | Entra desde | Qué controla size |
|---|---|---|---|
right | is-right | Derecha — el predeterminado | Ancho de la columna |
left | is-left | Izquierda | Ancho de la columna |
top | is-top | Arriba | Nada: el ancho es el de la pantalla y la altura, la del contenido |
bottom | is-bottom | Abajo | Nada: el ancho es el de la pantalla y la altura, la del contenido |
size | Ancho en los laterales |
|---|---|
sm | 18rem |
md | 24rem — el predeterminado |
lg | 34rem |
El panel lateral es una columna, no una tarjeta, y por eso su escala es distinta a la del modal. En el móvil (por debajo de 40rem)
se queda en min(20rem, 85vw) sea cual sea el tamaño: una columna estrecha ahí no se lee, y la franja que sobra
muestra que la página sigue detrás. Los botones del pie ocupan la fila, como en el modal.
El brillo nace del borde
En el modal la luz viene del centro de la pantalla. En el panel lateral sale del lado en el que se apoya, y la elipse es mayor: al nacer en el borde, solo la mitad queda en pantalla. Centrada, la luz apuntaría al medio de la página mientras el panel entra por el lateral.
Teclado y accesibilidad
Es un <dialog> abierto con showModal(), igual que el modal: la página de detrás queda inerte, el
foco no escapa y vuelve a quien lo abrió al cerrarse.
| Tecla | Acción |
|---|---|
Tab Shift+Tab | Recorre los controles del panel, sin volver a la página |
Enter Espacio | Activa el botón con foco |
Esc | Cierra con animación y devuelve el foco; no hace nada con closable: false |
Comparte el motor con el modal
Top layer, foco atrapado, Esc y foco devuelto viven en una sola base, y no en dos implementaciones parecidas. Eso es lo que impide que una corrección hecha en uno deje de valer en el otro.
Con title, el panel recibe aria-labelledby. La X es un .tuc-btn con
aria-label="Fechar". Con prefers-reduced-motion, la entrada dura 100 ms y no se desliza.
API
Generada a partir del código en cada build — si algo no está aquí, no 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 contentOpciones
Las notas de esta tabla salen de los comentarios del código fuente, que están escritos en portugués.
| Opción | Por defecto | 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 | '' |