Quatro painéis de largura cheia, irmãos, todos colados no mesmo topo. Conforme a página desce, cada um sobe e cobre o anterior por inteiro — não sobra faixa, não sobra sombra, e o fundo opaco faz a troca ler como corte de plano, não como card empilhando. Ao lado, um índice vertical também colado acende o capítulo corrente e leva a qualquer um dos quatro no clique. O olho vê um capítulo por vez; a mão escolhe qual.
Resolve a dobra em que uma empresa tem três a cinco frentes de atuação e cada uma precisa de espaço de página inteira: uma ilustração grande, uma lista de quatro entregas e um botão próprio. Em grade, quatro dessas viram quatro cartões de 300px em que a imagem morre e a lista não cabe. Em sequência simples, viram quatro telas que ninguém rola até o fim. Aqui cada frente ocupa a tela toda, uma de cada vez — e o índice existe justamente para que ninguém seja obrigado a passar pelas outras três para chegar na que interessa.
Quando usar
- Três a cinco frentes, serviços ou módulos que o visitante pode querer acessar fora de ordem. Se a ordem for obrigatória, o componente está mentindo — ver abaixo.
- Quando cada item pede, ao mesmo tempo, imagem grande + lista curta + chamada própria. É o teto que os irmãos não atendem.
- Página que já ganhou a atenção: a dobra custa cerca de três telas e meia de rolagem no desktop (medido: seção de 4 034px a 1440×900, ou 3,48 telas de rolagem além da primeira).
- Quando o visitante chega por link de anúncio ou de e-mail interessado em uma das frentes. O índice é o que faz isso funcionar sem uma página por serviço.
Quando não usar
- Etapas de um método que se executam em ordem.
listas/baralho-stickyfaz isso melhor e faz outra coisa: lá os cards coexistem — cada um deixa 14px do anterior à mostra e a pilha vira um baralho que acumula. A forma promete sequência e não deixa pular. Aqui a forma promete substituição e o índice deixa pular fora de ordem. São promessas opostas: use aquela para "passo 1 → passo 5", esta para "escolha por onde começar". - Narrativa que se lê para baixo, com um parágrafo de fôlego por bloco.
conteudo/rolagem-sincronizadaé a dobra certa: texto de 60 a 90 palavras rolando numa coluna e uma foto pinada que troca sozinha. Lá o visitante não tem controle, e é de propósito — a foto muda no canto do campo de visão enquanto ele lê. Aqui há um controle explícito, e quatro linhas de verificação não são narrativa. - Itens que se comparam lado a lado, ou itens cujo texto é uma frase só:
listas/leque-paineispõe os cinco na mesma tela, sem custar altura nenhuma — mas dá 440px fixos ao texto, sem lugar para lista nem botão por item. - Perguntas e respostas, ou qualquer coisa em que o corpo é só texto:
listas/acordeao-faqpula direto ao item por muito menos altura. - Panorama de recursos que precisa ser visto de uma vez:
conteudo/bento-recursos-expansivelmostra os seis juntos, com pesos diferentes. - Mais de cinco painéis. Cada painel custa um passo de 705px (medido a 1440×900). Cinco já passam de quatro telas, e o índice colado deixa de caber na altura de um painel. Seis é o ponto em que a dobra vira uma página.
- Quando a página já tem outra dobra presa ao scroll. Duas competem, e a segunda perde.
Padrão editorial
| Slot | Regra |
|---|---|
| Olho | 2 a 3 palavras. É o nome da série ("Frentes de atuação"), não uma frase. |
| Título da seção | 1 linha, uma palavra em <em>. Diz quantas frentes são: o número é o que prepara o índice. |
.sec-lead |
1 a 2 frases, até 40 palavras. Diz o que as frentes têm em comum e o que muda entre elas. |
| Selo | 2 palavras separadas por ·, repetidas para fechar a volta. Máximo ~53 caracteres na volta inteira, contando os separadores — o excedente não é desenhado e some sem aviso. Ver a ficha do efeitos/selo-circular. |
.pnl-nav-rot |
2 a 4 palavras, o mesmo texto da .pnl-marca do painel. É o rótulo do capítulo, não o título dele. |
.pnl-marca |
01 · Nome da frente. Numeração a partir de 01, em sequência, sem pular. |
.pnl-title |
6 a 10 palavras. É a promessa da frente, não o nome dela — o nome já está na pílula acima. Cabe em 3 linhas de 20ch; acima disso o bloco começa a apertar em janela baixa. |
.pnl-checks li |
Exatamente 4 linhas, de 6 a 12 palavras cada. Cada uma é uma entrega, não um adjetivo: "Política de investimentos por escrito, com faixa de alocação por classe", nunca "Atendimento de excelência". |
.pnl-cta |
2 a 4 palavras, verbo na frente. Um por painel, e cada um leva a um lugar diferente. |
Quatro linhas é estrutural, não estilo. A altura do painel é fixa e ele recorta o que sobra (ver gotchas). Uma quinta linha em qualquer painel some silenciosamente em janela de 900px. Precisa de cinco? Suba --pnl-h e refaça a conta de altura da dobra.
Os quatro títulos têm de ser paralelos. Todos afirmações ("A carteira sai de uma política escrita"), ou todos substantivos. Misturar as duas formas quebra a leitura da série — que aqui é a única coisa que liga quatro telas que se apagam uma à outra.
Especificação dos assets
- Quatro fotos retrato, mínimo 900×1200, WebP. Uma por painel.
- Peso alvo: até 180 KB cada. As quatro carregam à medida que a dobra sobe (
loading="lazy"), mas as três primeiras estão a menos de uma tela de distância — no pior caso três chegam juntas. - A foto sobrevive a dois recortes muito diferentes. No desktop a caixa é ~520×660 (proporção 0,79, quase o retrato original); no celular vira ~350×233 (proporção 1,5, uma paisagem tirada de um retrato). Assunto no terço central, com folga dos dois lados.
- Tratamento igual nas quatro: mesma temperatura, mesmo contraste. Elas nunca aparecem juntas, mas a troca é imediata, e diferença de tom entre duas aparece exatamente aí.
- O que não pode ter: texto embutido, marca d'água, logo de instituição real, rosto identificável de cliente real, ou assunto encostado na borda de cima — é por onde o painel de baixo entra.
<video>é variação legítima e é o que a referência usa. Um por painel,muted playsinline loopepreload="metadata"composter, e — importante — só o painel corrente deveria tocar: quatro vídeos decodificando ao mesmo tempo derrubam o celular. Sem essa disciplina, fique na foto.
Como usar
<link rel="stylesheet" href="componentes/listas/paineis-empilhados/estilo.css">
<script src="componentes/listas/paineis-empilhados/script.js"></script>
O selo do cabeçalho é o efeitos/selo-circular composto, não recopiado: cole também o estilo.css dele, ou apague o .pnl-selo-slot (variação "sem selo").
Um capítulo é sempre duas coisas, e as duas nomeiam o mesmo id:
<a class="pnl-nav-item" href="#pnl-carteira" data-pnl-alvo="pnl-carteira">…</a>
…
<article class="sec-deep pnl-painel" id="pnl-carteira" tabindex="-1" aria-labelledby="pnl-carteira-t">…</article>
Acrescentar ou remover um capítulo é acrescentar ou remover os dois juntos. Item de índice cujo id não existe é descartado pelo script em silêncio — nunca vira erro no console.
Ganchos
| Gancho | Papel |
|---|---|
[data-pnl] |
Raiz. O script parte daqui e nunca do documento — a dobra pode repetir na página. |
[data-pnl-alvo] |
No <a> do índice. O valor é o id do painel, e tem de bater com o href. |
aria-current="true" |
O item corrente. É atributo, não classe: o CSS lê o atributo, o leitor de tela lê a mesma coisa, e não há duas verdades para divergir. |
--pnl-top |
Onde painéis e índice encaixam. calc(var(--nav-h) + clamp(14px,2.4vh,28px)) — lê o menu do projeto. |
--pnl-fim |
Respiro sob o painel colado, e o vão entre painéis na pilha. Uma variável para as duas porque elas têm de andar juntas. |
--pnl-h |
Altura do painel. clamp(400px, 100vh − top − fim, 660px). É o principal parafuso: sobe a altura da dobra na mesma proporção. |
--pnl-gap |
Vão da pilha. Lê --pnl-fim; separe os dois só se souber por quê. |
--pnl-nav-w |
Largura do índice. 158px. |
--pnl-col |
Respiro entre índice e pilha. |
--pnl-col-arte |
Proporção arte : texto dentro do painel. 1.22fr contra 1fr. |
--pnl-trilho-h |
Altura do trilho horizontal do celular, medida (49px a 390×844). Só o scroll-margin-top do caso sem script a consome. |
Variações
- Três a cinco painéis — acrescente
<article>e item do índice juntos. - Vídeo no lugar da foto — ver a especificação de assets.
experimental: não rodou. - Índice à direita —
flex-direction:row-reversena.pnl-body, e inverta oinset-inline-startdo filete e aborder-inline-startda lista. - Em seção clara —
.sec-lightna seção e.sec-papernos painéis. A regra do tom vizinho continua valendo. - Sem selo — apague o
.pnl-selo-slote opreview-css.txt.
Compliance
A peça não devolve número nenhum, não compara cenários e não captura dado — mas ela promete entregas, e é isso que o consumidor precisa conferir antes de publicar:
- Cada linha de verificação é um compromisso operacional. "Comitê próprio revisa a política a cada trimestre" e "Assessor dedicado, com remuneração por taxa previamente acordada" são afirmações sobre o funcionamento da casa. Troque o que não for verdade na sua — a marca de verificação (✓) dá a essas linhas um peso de garantia que texto corrido não tem.
- Nenhuma linha fala de retorno, e nenhuma pode passar a falar. Rentabilidade, "ganho", "performance" e "acima do CDI" não entram aqui em hipótese alguma: o formato é uma lista de entregas, e uma promessa de resultado no meio de quatro entregas lê como entrega também.
- Zero superlativo. O título da referência trazia "o mais completo"; não atravessou, e não deve voltar.
- "Estimativa do custo de inventário no seu estado, em faixa" diz faixa de propósito. Se a sua peça entregar esse número, ela vira ferramenta e passa a valer a regra de faixa-nunca-linha do
dados/comparador-cenarios. - Os quatro botões apontam para
#no markup de exemplo. Cada um tem de ir a um destino real e diferente — quatro botões para a mesma página é promessa de profundidade que não existe. - Nada é gravado: não há
localStorage, cookie nem envio. O componente não tem estado que sobreviva ao recarregamento.
Gotchas
-
A pilha é CSS puro, e o script não empilha nada. Painéis irmãos, mesmo
top, altura menor que o contêiner, fundo opaco: sticky faz o resto, e a ordem de árvore decide quem cobre quem (elemento posicionado semz-indexpinta em ordem de documento). Se algum dia aparecer noscript.jsuma conta de posição de painel dentro de um listener de scroll, alguém reimplementou o sticky à mão — desfaça. -
padding-bottomna.pnl-pilhaNÃO dá percurso ao último painel, e esse foi o primeiro bug de verdade. Elemento sticky é preso ao bloco de conteúdo do contêiner, e padding fica fora dele. Medido a 1440×900 compadding-bottom:705px: o painel 04 encaixava e se soltava no mesmoscrollY2 437 (topo 36 em vez de 106, junto com os outros três) — os 705px contavam para a altura da página e nada para o percurso, e a última frente saía de cena sem ter sido lida. Quem resolve é.pnl-pilha::aftercomheight, que é conteúdo de verdade. Depois da troca, o painel 04 fica encaixado descrollY2 437 até o fim da rolagem (3 134), e a altura da seção não mudou um pixel. -
Um painel encaixado mente sobre onde está — em
getBoundingClientRect()E emoffsetTop. Medido: com os quatro encaixados a 1440×900, os quatro responderam o mesmo topo (2 555), inclusive os três que estavam cobertos. É por isso que a navegação para trás nasceu quebrada (voltar ao 01 depois do 04 rolava 4px), e é por isso quescrollIntoViewe a âncora nativa não servem para voltar. OtopoDeFluxo()reconstrói a posição a partir da.pnl-pilha, que não é sticky, somandooffsetHeightemargin-toppainel a painel — soma em vez dek × passoporque em janela baixa os painéis têm alturas diferentes. -
Sem JavaScript o índice leva para a frente, mas não volta. Medido no preview sem o
script.js: a âncora nativa para#pnl-relatoriolevou o painel 04 ao topo (topo 127, contra os 105,6 do encaixe — 21px de folga, e o painel fica inteiro na tela); a âncora de volta para#pnl-carteirafoi um no-op, pelo motivo do gotcha anterior. É degradação aceitável e não é conserto disponível em CSS: o conteúdo dos quatro painéis continua inteiro e legível rolando, e oscroll-margin-top:var(--pnl-top)é o que faz a ida parar no lugar certo em vez de embaixo do menu. -
A faixa vigiada e o limiar da decisão têm de ser o MESMO número. O
IntersectionObserverdispara no quadro em que o topo do painel entra numa faixa de 1px, e nesse quadro o painel ainda está alguns pixels abaixo do encaixe. Com a faixa em 107 e a decisão em 106,6 a marcação parava sempre um painel atrás — medido: clicar no 04 acendia o 03. Hoje os dois leemfaixaFim. -
Quem decide o corrente é a posição, não o
isIntersecting. A primeira versão marcava "o último painel que cruza a faixa", e isso quebra no celular: entre um painel e o outro há um vão de ~59px em que a faixa não cruza painel nenhum, e a marcação saltava para a última frente e voltava. A regra que vale nos três layouts é "o último painel cujo topo já passou da linha", e ela é recalculada só quando o observador avisa — algumas vezes por rolagem, nunca por quadro. -
A linha de troca é lida do
position, nunca dotop. Elemento estático guarda otopque o CSS declarou (a propriedade não se aplica, mas o valor computado continua lá). A primeira versão lia 105,6px no celular, onde os painéis são estáticos, e mandava o painel parar 33px por baixo do trilho colado. O CSS devolvetop:autonos dois blocos que desligam a pilha, e o script confereposition— duas pistas dizendo a mesma coisa. -
overflowdiferente devisibleem qualquer ancestral mata a pilha. É a mesma armadilha doconteudo/rolagem-sincronizada. Por isso não háoverflow:hiddenna seção, no.wrap, no.pnl-bodynem na.pnl-pilha— verificado por medição: o único ancestral não-visibledo painel é o<body>, com ooverflow-x:hiddendo core, que o navegador propaga para a viewport e não conta. Ooverflow:hiddendo próprio.pnl-painelé seguro: overflow recorta descendente, não a si mesmo. -
O painel RECORTA o que não cabe, e isso é escolha. Sem o recorte, um texto mais alto que
--pnl-htransbordaria por cima do painel de baixo — texto sobre texto, na frente de todo mundo. Recortar contém o estrago dentro do painel, ao custo de sumir com uma quinta linha em silêncio. É o motivo de o padrão editorial cravar quatro. -
Abaixo de 700px de altura de janela a pilha se desliga sozinha, e não é conservadorismo. A conta que a pilha precisa fechar é
--pnl-top + --pnl-h + --pnl-fim ≤ 100vh, com--pnl-hgrande o bastante para o conteúdo. Medido a 1440×620: o painel cabia em 490px e o bloco de texto pedia 554 — o botão sumia dentro do recorte. Um notebook de 1366×768 com barra de menu e abas cai exatamente aí. A saída não foi apertar o texto: abaixo de 699px os painéis viram sequência de altura automática e o índice continua colado ao lado, navegável. Perde-se a troca; não se perde conteúdo nem o acesso fora de ordem. -
O respiro do bloco de texto é
vhem cima e embaixo,vwnos lados, e a assimetria é deliberada: em janela baixa quem falta é altura, e um padding emvwcontinuaria gastando 46px em cima e embaixo de um painel de 490. -
Em janela baixa a
<img>passava a mandar na altura do painel. Sem altura fixa,block-size:100%numa linha de grid automática cai na proporção natural da foto: medido a 1440×620, um painel que precisava de 514px saía com 657. A arte viraposition:absolutenesse bloco, deixa de pedir altura, e quem decide volta a ser o texto. -
O
data-rvnunca vai no<article>. Não porque quebre o sticky — não quebra —, mas porque o painel é a peça que o CSS gerencia, e o §5 do contrato manda separar as duas coisas. Quem entra com blur é o conteúdo:.pnl-artee.pnl-texto, escalonados. Medido: os 11 elementos comdata-rvacendem, e com movimento reduzido os 11 nascem visíveis. -
A pilha continua funcionando com movimento reduzido, e isso é decisão. Ela é leitura 1:1 do gesto de rolar, como uma barra de rolagem — nenhum sistema operacional esconde a barra sob
reduce. O que a preferência desliga, medido: o giro do selo (animation-name:none), o deslize do filete do índice (transition-duration:0s), a entrada com blur, e a rolagem suave do clique — que passa a ser'instant'e chega ao destino em menos de 60ms, contra os 32px percorridos no mesmo tempo com a rolagem suave ligada. -
behavior:'instant'e não'auto'. O core declarahtml{scroll-behavior:smooth}, e'auto'herdaria a rolagem suave justamente de quem pediu para não ter movimento. Rolar continua acontecendo — foi pedido —, só sem a animação. -
O índice não se marca no clique. Quem marca é sempre o observador. Durante a rolagem suave o índice acende cada painel por onde passa, que é o que de fato está na tela; marcar no clique criaria uma segunda verdade que a primeira desfaz meio segundo depois.
-
O clique não sequestra
ctrl,cmd,shiftnem o botão do meio. O item do índice é um link de verdade comhrefde verdade: abrir em nova aba é decisão de quem clicou. -
O foco vai para o painel, não fica no item do índice. É o comportamento de um link de pular conteúdo: medido,
Tabdepois do clique cai no botão daquele capítulo, e não no próximo item do índice. Por isso cada<article>levatabindex="-1"— ele não entra na ordem de tabulação, só recebe foco por programa. -
Dois destes na mesma página exigem trocar os ids — dos painéis, dos títulos e do arco do selo (
pnl-selo-arco). O script já parte da raiz e aguenta a repetição; os ids é que são do documento. Mesmo cuidado da ficha doefeitos/selo-circular. -
O trilho horizontal do celular tem
padding-block:7pxe isso não é estética.overflow-x:autoimplicaoverflow-y:auto: sem o respiro, o anel de foco (outline-offset:3pxdo core) gerava rolagem vertical dentro do trilho. Medido a 390×844:scrollHeight − clientHeight = 0. -
Contraste, no tema Liberta (medido): título 15,6:1 e linhas de verificação 10,63:1 sobre o painel; índice em repouso 6,05:1, em hover 11,85:1 e corrente 17,39:1 sobre a seção; a pílula do capítulo 5,43:1; a marca de verificação e o número do item corrente 12,08:1 em
--accent. Todos acima do mínimo AA, inclusive o estado de repouso do índice — que é texto pequeno e o primeiro a cair quando alguém troca o tema.
Histórico
| Projeto | Onde | Data | Diferenças |
|---|---|---|---|
| referência: contasonline.com.br | https://www.contasonline.com.br/ — seção .s-recursos, "Principais recursos" |
2026-09-06 | Clone reescrito do zero. Nenhuma linha de código veio de lá. |
Referência visual, nunca origem de código. A seção foi medida no DOM e reescrita aqui dentro. Da referência vieram três coisas e só três: a ideia da interação (painéis irmãos sticky no mesmo top, com índice lateral também colado), a proporção (painel de largura cheia com arte de um lado e lista de verificação do outro) e o ritmo (~680px de rolagem por painel de 512px, num índice de 104px com itens de 112px). O arquivo é nosso, em token, prefixo, primitiva do core e gancho do motion.js. Registro consolidado em projetos/referencias-externas.md, seção Clones.
O que a medição desmentiu. À primeira vista a seção parece um conjunto de abas clicadas. Não é: são quatro painéis irmãos, todos position:sticky; top:96px, numa seção de 3 127px para 4 painéis de 512px. O .ativo do índice não mudava durante a medição e o clique não trocava nada — não porque o site esteja quebrado, mas porque o painel de navegador embutido congela rAF e observador. Para o clone, o comportamento correto era o óbvio, e é o que está aqui: o índice acende conforme o painel correspondente cobre, e clicar rola até ele.
O que não atravessou. As fontes (Jakarta e Inter da referência → --display/--sans da casa), os ícones das abas (não atravessam — viraram numeração 01…04, que é o idioma da casa), o <video> proprietário (placeholder de showroom/demo/, com a especificação do asset real acima), o verde de marca (--accent do tema, zero cor crua) e o superlativo do título.
Decisões de tradução para o contrato.
rel="gestao"como âncora era abuso de atributo — um valor inventado num atributo que não aponta para nada. Aqui édata-pnl-alvocom o id de um elemento que existe, e o script descarta o item cujo alvo sumiu.<button>virou<a href="#id">, e essa é a divergência de forma mais importante. Na referência o índice é um<button>: sem script, um controle morto que promete navegação e não entrega. Como link, ele leva ao painel com o JavaScript desligado (oscroll-margin-topfaz a ida parar no lugar certo) e o script só melhora o que já funciona — controlar a curva e respeitarreduce. O custo está medido no gotcha da navegação para trás.aria-current="true"no lugar de uma classe.ativo. O estado é atributo, o CSS lê o atributo, e o leitor de tela recebe a mesma informação que o olho. Não existe segunda fonte de verdade.- O escopo de tom vem do markup: painéis
.sec-deepdentro de uma seção.sec-dark. O componente não sobrescreve--fgem lugar nenhum — só declara em qual escopo cada painel vive. Precedente e permissão:MANUAL.md§3 econteudo/bento-recursos-expansivel. Os dois escuros são vizinhos de propósito, o que faz a faixa livre abaixo do painel colado (134px a 1440×900) desaparecer em vez de virar um buraco. - O selo do cabeçalho é composição, não cópia: o
efeitos/selo-circularentra pelopreview-css.txt, com um id de arco próprio. Nenhuma linha do desenho dele foi recopiada. - O que virou gancho do core: a entrada é
data-rv/data-de.ttl; a pílula do capítulo é a.pill; o botão é.btn/.btn-primary; o ruído sobre a foto é o.grain; o texto do selo para leitor de tela é.visually-hidden; a coluna é o.wrape o ritmo é o.sec-pad. Otopdo sticky lê--nav-hem vez de um número, que é o que faz a dobra funcionar em projeto com menu alto e baixo sem tocar no CSS. - Zero listener de scroll, e nenhuma biblioteca. O padrão costuma vir com GSAP
ScrollTrigger; aqui o empilhamento éposition:stickye a marcação é umIntersectionObserverque dispara no pixel do encaixe. Não há conta por quadro em lugar nenhum.
Divergências conhecidas vs. a referência: a referência tem quatro painéis de altura fixa (512px) e um índice com ícones; aqui a altura é clamp da janela (400–660px), o índice é numerado, e há dois layouts que a referência não tem — o trilho horizontal colado do celular (a referência simplesmente empilha em coluna, m:flex-col) e o desligamento da pilha em janela abaixo de 700px de altura.