Tucano v0.37.2

Accordion

Blocks that collapse and expand, built on <details> and <summary>. The native element already handles keyboard, semantics and state, and opens and closes before JavaScript loads. The component only steps in where native can't go: animation.

Do I need a build step in my project?

No. The CSS ships compiled and Tailwind is used only inside the library. On your side it's two files and nothing else.

Does it work with HTMX?

Yes. Whatever arrives through a swap initializes on its own, so an accordion coming from the server is animated from the start.

What if JavaScript fails?

This accordion keeps opening and closing: it's a native <details>. The only thing you lose is the animation.

Examples

The same <details>, with one extra attribute or class.

Several open

Without data-single, each item opens and closes on its own.

Contract details

Number 2026-0142, valid for 12 months.

Parties

Tucano Trading Ltd and Warm Bread Bakery.

Clauses

Yearly inflation adjustment and a 2% late fee.

One at a time

data-single="true" collapses the others when one opens.

Monthly plan

Billed on the 10th of every month, cancel anytime.

Yearly plan

Two months off, paid up front.

Custom

For more than 50 users.

Real content inside

The height is measured by the browser, not guessed: forms, tables and images open the same way.

Report filters
Visible columns

No dividers, for menus

is-plain removes the lines and makes the title look like a group label.

How to use

The HTML is the usual one. The <details> elements must be direct children of the marked element, and the open written in the template says which ones start open.

<div class="tuc-accordion" data-tuc-accordion data-single="true">
  <details open>
    <summary>Projects</summary>
    <p>List, create and track inspections.</p>
  </details>
  <details>
    <summary>Reports</summary>
    <p>Monthly, by team and by period.</p>
  </details>
</div>

The script adds what's missing: the tuc-accordion class on the container, tuc-accordion__item on each <details>, tuc-accordion__trigger on the <summary>, the chevron, and it wraps the rest of the content in tuc-accordion__body and tuc-accordion__content. Until then, the CSS already draws the raw accordion with the same lines and the same spacing, so nothing jumps when JavaScript arrives.

In the Django template

<div class="tuc-accordion" data-tuc-accordion data-single="true">
  {% for question in questions %}
    <details{% if forloop.first %} open{% endif %}>
      <summary>{{ question.title }}</summary>
      {{ question.answer|linebreaks }}
    </details>
  {% endfor %}
</div>

In JavaScript

const faq = new Tucano.Accordion('#faq', { single: true });

const item = document.querySelector('#faq > details:nth-child(2)');
faq.open(item);      // with single, collapses the others
faq.close(item);     // animates, and only then removes open
faq.items;           // the <details>, in order
faq.destroy();

How it animates

The content height is auto, and auto doesn't transition. On close, the browser also hides the content in the same frame in which open is removed.

Opening is pure CSS: the body is a one-row grid, and the row animates from 0fr to 1fr — interpolable, with no measuring in JavaScript and no fixed height. Closing needs a script: the click on the <summary> is intercepted, the item gets is-closing and stays open while the row goes back to 0fr, and only at the end is open removed. Reopening mid-close cancels the exit and continues from wherever the height was.

The end of the animation comes from transitionend, not from a number

A timer in JavaScript would have to mirror the CSS token, and the two drift apart: 220 ms against 280 ms pulled the content out before the end, and the close looked cut off. The item closes when the grid row finishes transitioning; a 500 ms timeout stays only as a safety net, for a hidden tab or reduced motion, when the event never arrives.

The bottom spacing is margin, not padding

An fr track doesn't shrink below the content's minimum, and padding counts toward that minimum: with padding on the content, the closed item kept a 14px gap. That's why the space comes from the last child's margin, which overflow: hidden clips down to zero. If you style the content, keep that rule.

The first opening of a <details> tends to stutter, because the browser hasn't measured what was closed yet. On init, the component opens and closes each item in the same synchronous block — nothing gets painted — just to get that calculation out of the way. With prefers-reduced-motion, the transition drops to 1 ms.

Keyboard and accessibility

Everything here comes from <details>: the <summary> is focusable, and the screen reader announces collapsed and expanded without any aria-* of ours.

KeyAction
TabMoves from title to title, and into the content of open items
Enter SpaceOpens or closes the focused item, with the same animation as a click

The chevron is decoration and carries aria-hidden, so it isn't read along with the title. A closed <details> has nothing focusable in the Tab path: a field inside a collapsed item can't be reached until someone opens the item.

API

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

Markup
[data-tuc-accordion]
In JS
new Tucano.Accordion(alvo, opcoes)
Attributes
data-single
Methods
open close destroy

Options

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

OptionDefaultWhat for
singlefalseabrir um recolhe os outros