← Biblioteca componentes/listas/acordeao-faq/ficha.md CódigoTela cheia
listas/acordeao-faq

Acordeão de perguntas frequentes

🟢 estavel complexidade baixa prefixo faq- faq acordeao duvidas objecoes acessibilidade

Incluir · as linhas que se colam num site novo, depois do core (CLAUDE.md §10)

<link rel="stylesheet" href="componentes/listas/acordeao-faq/estilo.css">
<script src="componentes/listas/acordeao-faq/script.js"></script>

markup.html · 60 linhas · 3,0 KB

<!-- ==========================================================================
     FAQ — acordeão de perguntas frequentes
     Prefixo: faq-
     Requer: core (tokens, base, motion.css, motion.js) + estilo.css e script.js

     Um item aberto por vez. Cada pergunta é um <button> de verdade, com
     aria-expanded e aria-controls — leitor de tela precisa saber que aquilo
     abre e o que abre.
     ========================================================================== -->

<section class="sec-dark sec-pad" id="duvidas">
  <div class="wrap">
    <div class="sec-head">
      <span class="eyebrow" data-rv>Perguntas frequentes</span>
      <h2 class="sec-title ttl">Dúvidas comuns sobre a consultoria</h2>
    </div>

    <div class="faq" data-faq data-rv data-d="1">
      <div class="faq-item">
        <button class="faq-question" aria-expanded="false" aria-controls="faq-a1">
          Como funciona o processo de consultoria?
          <span class="faq-sign" aria-hidden="true"></span>
        </button>
        <div class="faq-answer" id="faq-a1" role="region">
          <p>Começamos com um diagnóstico inicial, mapeando sua estrutura patrimonial atual e seus objetivos futuros. A partir disso construímos uma Política de Investimentos personalizada, implementamos o plano e mantemos acompanhamento contínuo, com revisões periódicas do portfólio.</p>
        </div>
      </div>

      <div class="faq-item">
        <button class="faq-question" aria-expanded="false" aria-controls="faq-a2">
          Qual é o investimento inicial necessário?
          <span class="faq-sign" aria-hidden="true"></span>
        </button>
        <div class="faq-answer" id="faq-a2" role="region">
          <p>O diagnóstico gratuito é indicado para investidores com patrimônio a partir de R$ 3 milhões. Para volumes menores, converse com o time — podemos indicar o caminho mais adequado.</p>
        </div>
      </div>

      <div class="faq-item">
        <button class="faq-question" aria-expanded="false" aria-controls="faq-a3">
          Como vocês garantem a segurança do meu patrimônio?
          <span class="faq-sign" aria-hidden="true"></span>
        </button>
        <div class="faq-answer" id="faq-a3" role="region">
          <p>Somos uma consultoria registrada e autorizada pelo órgão regulador. Não custodiamos recursos — seus ativos permanecem sempre nas instituições financeiras de sua escolha, com total transparência de movimentação.</p>
        </div>
      </div>

      <div class="faq-item">
        <button class="faq-question" aria-expanded="false" aria-controls="faq-a4">
          Como vocês se remuneram pelos serviços prestados?
          <span class="faq-sign" aria-hidden="true"></span>
        </button>
        <div class="faq-answer" id="faq-a4" role="region">
          <p>Trabalhamos com um modelo comercial fee-based, previamente acordado com cada cliente, sem comissões ocultas — o que garante alinhamento total de interesses ao longo da parceria.</p>
        </div>
      </div>
    </div>
  </div>
</section>

estilo.css · 80 linhas · 3,4 KB

/* ==========================================================================
   FAQ — acordeão de perguntas frequentes
   Prefixo: faq-
   Tokens usados: --bg --fg --fg-2 --rule --accent --display --ease --nav-h
                  --radius-media
   ========================================================================== */


/* 840px e não a coluna inteira: linha de resposta acima de ~75 caracteres
   cansa, e a pergunta centralizada precisa de bloco mais estreito para não
   ficar solta no meio da tela. */
.faq{max-width:840px;margin-inline:auto;margin-top:clamp(34px,5vh,54px)}

.faq-item{border-top:1px solid var(--rule)}
.faq-item:last-child{border-bottom:1px solid var(--rule)}

.faq-question{
  width:100%;background:none;border:none;color:var(--fg);text-align:left;
  padding:24px 2px;display:flex;justify-content:space-between;align-items:center;gap:20px;
  font-family:var(--display);font-weight:700;font-size:15px;letter-spacing:-.02em;
  transition:color .3s;
}
.faq-question:hover{color:var(--accent)}

/* Sinal +/− em duas pseudo-barras: a vertical gira 90° e vira traço. Um
   ícone trocado por outro piscaria; a rotação é contínua. */
.faq-sign{flex:0 0 auto;position:relative;width:13px;height:13px}
.faq-sign::before,.faq-sign::after{
  content:"";position:absolute;background:var(--accent);
  transition:transform .4s var(--ease);
}
.faq-sign::before{left:0;top:6px;width:13px;height:1px}
.faq-sign::after{left:6px;top:0;width:1px;height:13px}
.faq-item.is-open .faq-sign::after{transform:rotate(90deg)}

/* max-height animado, com o valor real medido pelo script. Não dá para
   animar height:auto, e um max-height chutado alto deixa a abertura lenta
   e o fechamento com atraso. */
.faq-answer{max-height:0;overflow:hidden;transition:max-height .5s var(--ease)}
.faq-answer p{padding:0 2px 24px;font-size:17px;line-height:1.6;color:var(--fg-2);max-width:72ch}

@media(prefers-reduced-motion:reduce){
  .faq-answer{transition:none}
  .faq-sign::before,.faq-sign::after{transition:none}
}

/* ---------------------------------------------------------------- variação: com cartão de contato
   As perguntas dividem a dobra com um cartão ao lado: "não achou? fale com a
   gente", e dois caminhos. Quem chega ao fim do FAQ sem resposta é
   exatamente quem converteria por contato — o cartão fica onde a dúvida
   acaba. Referência visual: ruul.io (via cta.gallery).

   O cartão é sticky e acompanha a leitura das perguntas. Dentro de um
   [data-rv] isso continua funcionando: filtro prende descendente `fixed`,
   não `sticky` (MANUAL §5). A largura do FAQ sai do teto de 840px porque a
   coluna agora é dividida. */
.faq-duo{
  display:grid;grid-template-columns:minmax(0,1fr) minmax(260px,340px);
  gap:clamp(28px,4vw,56px);align-items:start;
  margin-top:clamp(34px,5vh,54px);
}
.faq-duo .faq{max-width:none;margin:0}
.faq-cartao{
  position:sticky;top:calc(var(--nav-h) + 24px);
  display:grid;gap:14px;
  padding:clamp(22px,2.4vw,30px);
  border:1px solid var(--rule);border-radius:var(--radius-media);
  background:color-mix(in srgb,var(--fg) 5%,var(--bg));
}
.faq-cartao-t{
  font-family:var(--display);font-weight:700;font-size:22px;line-height:1.2;
  letter-spacing:-.02em;color:var(--fg);
}
.faq-cartao-p{font-size:14.5px;line-height:1.6;color:var(--fg-2)}
.faq-cartao-acoes{display:flex;flex-wrap:wrap;gap:10px;margin-top:6px}

@media(max-width:860px){
  .faq-duo{grid-template-columns:1fr}
  .faq-cartao{position:static}
}

script.js · 46 linhas · 1,9 KB

/* ==========================================================================
   FAQ — acordeão de perguntas frequentes
   Um item aberto por vez, por acordeão. Mede a altura real da resposta para
   o max-height animar de verdade.
   Suporta mais de um FAQ na página: parte de [data-faq].
   ========================================================================== */

(function () {
  document.querySelectorAll('[data-faq]').forEach(function (faq) {
    var items = [].slice.call(faq.querySelectorAll('.faq-item'));

    function fechar(item) {
      item.classList.remove('is-open');
      item.querySelector('.faq-answer').style.maxHeight = null;
      item.querySelector('.faq-question').setAttribute('aria-expanded', 'false');
    }

    function abrir(item) {
      var resposta = item.querySelector('.faq-answer');
      item.classList.add('is-open');
      /* scrollHeight é medido no momento do clique, não no load: se a fonte
         ainda não tinha carregado, a altura estaria errada. */
      resposta.style.maxHeight = resposta.scrollHeight + 'px';
      item.querySelector('.faq-question').setAttribute('aria-expanded', 'true');
    }

    items.forEach(function (item) {
      item.querySelector('.faq-question').addEventListener('click', function () {
        var aberto = item.classList.contains('is-open');
        items.forEach(function (outro) {
          if (outro !== item && outro.classList.contains('is-open')) fechar(outro);
        });
        aberto ? fechar(item) : abrir(item);
      });
    });

    /* Se a janela mudar de largura, a resposta aberta muda de altura — o
       max-height congelado a cortaria. */
    addEventListener('resize', function () {
      var aberto = faq.querySelector('.faq-item.is-open');
      if (!aberto) return;
      var resposta = aberto.querySelector('.faq-answer');
      resposta.style.maxHeight = resposta.scrollHeight + 'px';
    }, { passive: true });
  });
})();

Lista de perguntas separadas por régua, cada uma abrindo em altura animada. O sinal à direita é um + cuja barra vertical gira 90° e vira −. Um item aberto por vez.


Quando usar

Quando não usar


Padrão editorial

Slot Regra
Pergunta Escrita na voz do cliente, na primeira pessoa: "Qual é o investimento inicial necessário?", não "Investimento mínimo". 6 a 12 palavras, uma linha só.
Resposta 1 a 3 frases, 30 a 60 palavras, teto de 72ch. Responde direto na primeira frase; o resto é contexto.

Ordem: da objeção mais comum para a mais rara. A primeira pergunta é quase sempre "como funciona" e a última é "como começo".

Nunca responda com "depende". Se depende, diga do quê e dê a faixa.


Como usar

<link rel="stylesheet" href="componentes/listas/acordeao-faq/estilo.css">
<script src="componentes/listas/acordeao-faq/script.js"></script>

Cada resposta precisa de id único, e a pergunta correspondente aponta para ele em aria-controls.

Variações

<div class="faq-duo">
  <div class="faq" data-faq>…os itens…</div>
  <aside class="faq-cartao" aria-label="Fale com a gente">
    <p class="faq-cartao-t">Não achou a sua pergunta?</p>
    <p class="faq-cartao-p">Um sócio responde por escrito em até um dia útil.</p>
    <div class="faq-cartao-acoes">
      <a class="btn btn-primary" href="#agendar">Agendar</a>
      <a class="btn btn-ghost" href="mailto:contato@exemplo.invalid">Escrever</a>
    </div>
  </aside>
</div>

Referência visual: ruul.io (via cta.gallery), 2026-09-29.


Gotchas


Histórico

Projeto Onde Data Diferenças
Liberta Wealth index.html #duvidas 2026-08 Origem, com 8 perguntas. Classes .faq-i, .faq-q, .faq-a, .pm, estado .open, script varrendo .faq-i global, sem nenhum atributo ARIA, sem recálculo no resize.

Normalizações aplicadas na extração: .faq-i → .faq-item, .faq-q → .faq-question, .faq-a → .faq-answer, .pm → .faq-sign; .open → .is-open; script passou a partir de [data-faq], suportando vários acordeões na página; adicionados aria-expanded, aria-controls e role="region"; adicionado recálculo de altura no resize; adicionado prefers-reduced-motion.