Acordeão
Blocos que recolhem e expandem, feitos sobre <details> e <summary>. O nativo
já resolve teclado, semântica e estado, e abre e fecha antes de o JavaScript carregar. O componente entra só onde o
nativo não vai: animar.
Preciso de build no meu projeto?
Não. O CSS já vai compilado e o Tailwind é usado só dentro da biblioteca. Do seu lado são dois arquivos e nada mais.
Funciona com HTMX?
Sim. O que chega por swap é inicializado sozinho, então o acordeão vindo do servidor já nasce animado.
E se o JavaScript falhar?
Este acordeão continua abrindo e fechando: ele é <details> nativo. O que se perde é só a animação.
Exemplos
O mesmo <details>, com um atributo ou uma classe a mais.
Vários abertos
Sem data-single, cada item abre e fecha por conta própria.
Dados do contrato
Número 2026-0142, vigência de 12 meses.
Partes
Tucano Comércio Ltda e Padaria Pão Quente.
Cláusulas
Reajuste anual pelo IPCA e multa de 2% por atraso.
Um de cada vez
data-single="true" recolhe os outros ao abrir um.
Plano mensal
Cobrança todo dia 10, cancele quando quiser.
Plano anual
Dois meses de desconto, pago de uma vez.
Sob consulta
Para mais de 50 usuários.
Conteúdo de verdade dentro
A altura é medida pelo navegador, não chutada: formulário, tabela e imagem abrem igual.
Filtros do relatório
Colunas visíveis
Sem divisórias, para menu
is-plain tira as linhas e deixa o título com cara de rótulo de grupo.
Cadastros
Financeiro
Como usar
O HTML é o de sempre. Os <details> precisam ser filhos diretos do elemento marcado, e o
open escrito no template diz quais nascem abertos.
<div class="tuc-accordion" data-tuc-accordion data-single="true">
<details open>
<summary>Projetos</summary>
<p>Listar, criar e acompanhar vistorias.</p>
</details>
<details>
<summary>Relatórios</summary>
<p>Mensal, por equipe e por período.</p>
</details>
</div>O script acrescenta o que falta: a classe tuc-accordion no contêiner, tuc-accordion__item em cada
<details>, tuc-accordion__trigger no <summary>, a seta, e embrulha o resto do
conteúdo em tuc-accordion__body e tuc-accordion__content. Até lá, o CSS já desenha o acordeão cru com
as mesmas linhas e o mesmo respiro, para nada pular quando o JavaScript chega.
No template do Django
<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>Em JavaScript
const faq = new Tucano.Accordion('#faq', { single: true });
const item = document.querySelector('#faq > details:nth-child(2)');
faq.open(item); // com single, recolhe os outros
faq.close(item); // anima e só então tira o open
faq.items; // os <details>, na ordem
faq.destroy();Como anima
A altura do conteúdo é auto, e auto não transiciona. Ao fechar, o navegador ainda some
com o conteúdo no mesmo quadro em que o open cai.
A abertura é CSS puro: o corpo é um grid de uma linha, e a linha anima de 0fr a 1fr — interpolável, sem
medir nada em JavaScript nem fixar altura. O fechamento precisa de script: o clique no <summary> é
interceptado, o item ganha is-closing e continua aberto enquanto a linha volta a 0fr, e só no fim o
open cai. Reabrir no meio do fechamento cancela a saída e segue do ponto em que a altura estava.
O fim da animação vem do transitionend, não de um número
Um tempo em JavaScript teria de espelhar o token do CSS, e os dois divergem: 220 ms contra 280 ms arrancava o conteúdo antes do fim, e o fechamento aparecia cortado. O item fecha quando a linha do grid termina de transicionar; um timeout de 500 ms fica só como rede de segurança, para aba oculta ou movimento reduzido, quando o evento não chega.
O respiro de baixo é margem, não padding
Uma faixa em fr não encolhe abaixo do mínimo do conteúdo, e o padding entra nesse mínimo: com padding no conteúdo, o item fechado ficava com uma fresta de 14px. Por isso o espaço sai da margem do último filho, que o overflow: hidden recorta até zero. Se estilizar o conteúdo, mantenha a regra.
A primeira abertura de um <details> costuma engasgar, porque o navegador ainda não mediu o que estava fechado.
Ao iniciar, o componente abre e fecha cada item no mesmo bloco síncrono — nada chega a pintar — só para adiantar esse
cálculo. Com prefers-reduced-motion, a transição cai para 1 ms.
Teclado e acessibilidade
Tudo aqui vem do <details>: o <summary> é focável, e o leitor de tela anuncia
recolhido e expandido sem nenhum aria-* nosso.
| Tecla | Ação |
|---|---|
Tab | Anda de título em título, e para dentro do conteúdo dos itens abertos |
Enter Espaço | Abre ou fecha o item focado, com a mesma animação do clique |
A seta é desenho e leva aria-hidden, para não ser lida junto do título. Um <details> fechado não
tem nada focável no caminho do Tab: o campo de um item recolhido não é alcançado até alguém abrir o item.
API
Gerada do código a cada build — se algo não está aqui, não existe.
[data-tuc-accordion]new Tucano.Accordion(alvo, opcoes)data-singleopen close destroyOpções
| Opção | Padrão | Para quê |
|---|---|---|
single | false | abrir um recolhe os outros |