Tucano v0.37.2

Drawer

A panel that enters from an edge and takes that edge's whole axis: list filters, mobile menu, cart, record details. It shares with the modal the mechanics of <dialog> — what changes is the geometry and the motion: in the center the dialog grows, at the edge it slides, because that is how the drawer tells you where it came from.

Filters

Narrow down the contract list.

Status

Examples

Every example below is the same Tucano.drawer() with different options.

The four sides

side picks the edge; right is the default.

Column width

size on the sides: 18, 24 or 34rem.

Tones

tone changes the glow, which starts from the edge the drawer sits against.

No closing from outside

closeOnBackdrop: false and closable: false, as in the modal.

Free content

content(node) fills the body, which scrolls when the content gets taller than the screen.

App menu

A drawer from the left with the navigation list inside — the full example is on the side menu page.

How to use

The same two ways as the modal. From JavaScript the drawer is built on the spot and leaves the DOM when it closes; from the template it is already on the page, and the component only opens, closes and animates it.

In JavaScript

Tucano.drawer({
  title: 'Filters',
  text: 'Narrow down the contract list.',
  side: 'right',      // left | right | top | bottom
  size: 'md',         // sm | md | lg — on the sides, column width
  tone: 'default',    // default | danger | success | warning
  actions: [
    { text: 'Clear', variant: 'ghost' },
    { text: 'Apply', variant: 'primary', onClick: (drawer) => apply() },
  ],
}).content(form);

// Built first, opened later
const drawer = new Tucano.Drawer({ title: 'Cart', side: 'right' });
drawer.content(list).open();
drawer.close();

actions follows the same rules as the modal: text, variant (default outline), onClick, which receives the instance, and closes: false to keep it open. onClose(reason, drawer) receives 'button', 'action', 'escape', 'backdrop' or 'api'.

In the Django template

The typical case is a list's filter form: it is rendered by the server, with the values that came in request.GET, and the drawer just hides it until someone asks for it.

<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">Filters</h2>
      </div>
      <button type="button" class="tuc-btn is-ghost is-icon is-sm tuc-drawer__close" aria-label="Close" 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="?">Clear</a>
      <button class="tuc-btn is-primary">Apply</button>
    </div>
  </form>
</dialog>

<button class="tuc-btn is-outline" data-tuc-drawer="#filters">Filters</button>
AttributeWhereWhat for
data-tuc-drawer="#id"TriggerOpens the drawer matching the selector on click
data-tuc-drawer-closeButton inside the drawerCloses it, with the reason 'button'
data-closable="false"<dialog>Esc no longer closes it
data-backdrop="false"<dialog>Clicking the backdrop no longer closes it

Every dialog.tuc-drawer is adopted on load and on every htmx:afterSwap. The inner classes follow the component name: __panel, __top, __header, __title, __text, __close, __body and __footer. In the template, write the X and aria-labelledby: from JavaScript both come ready-made, by hand they don't.

Sides and sizes

The box stretches along the axis of the edge it sits against — on the sides it takes the full height, at the top and bottom the full width — and the box's border appears only on the side that touches the page.

sideClassEnters fromWhat size controls
rightis-rightRight — the defaultColumn width
leftis-leftLeftColumn width
topis-topTopNothing: the width is the screen's and the height, the content's
bottomis-bottomBottomNothing: the width is the screen's and the height, the content's
sizeWidth on the sides
sm18rem
md24rem — the default
lg34rem

A side drawer is a column, not a card, so its scale differs from the modal's. On mobile (below 40rem) it stops at min(20rem, 85vw) whatever the size: a narrow column is unreadable there, and the strip left over shows that the page is still behind it. The footer buttons take the full row, as in the modal.

The glow starts from the edge

In the modal the light comes from the center of the screen. In the drawer it comes from the side the panel sits against, and the ellipse is larger: starting at the edge, only half of it stays on screen. Centered, the light would point at the middle of the page while the panel slides in from the side.

Keyboard and accessibility

It is a <dialog> opened with showModal(), just like the modal: the page behind becomes inert, focus can't escape and it returns to whoever opened it on close.

KeyAction
Tab Shift+TabMoves between the drawer's controls, without going back to the page
Enter SpaceActivates the focused button
EscCloses with animation and returns focus; does nothing with closable: false

Shares the engine with the modal

Top layer, trapped focus, Esc and returned focus live in a single base, not in two similar implementations. That is what keeps a fix made in one of them from failing to apply to the other.

With title, the drawer gets aria-labelledby. The X is a .tuc-btn with aria-label="Fechar". With prefers-reduced-motion, the entrance lasts 100 ms and doesn't slide.

API

Generated from the code on every build — if something is not here, it does not exist.

Markup
dialog.tuc-drawer [data-tuc-drawer-close] [data-tuc-drawer]
In JS
new Tucano.Drawer(opcoes) Tucano.drawer()
Attributes
data-backdrop data-closable data-tuc-drawer
Methods
open close content

Options

The notes in this table come from comments in the source code, which are written in Portuguese.

OptionDefaultWhat for
titlenull
text''
side'right'left | right | top | bottom
size'md'sm | md | lg — nas laterais, largura da coluna
tone'default'default | danger | success | warning
closabletrue
closeOnBackdroptrue
actionsnull[{ text, variant, onClick, closes }] — closes:false mantem aberto
onClosenull
className''