Duas colunas. À esquerda o argumento da dobra — olho, título, apoio — e, embaixo dele, dois botões redondos com um contador 1 / 5. À direita, cinco fotos em retrato empilhadas uma sobre a outra, cada uma 12px mais abaixo, 4,5% menor e levemente inclinada, como cartas largadas na mesa. O card de cima acompanha o dedo e gira com o gesto, girando a partir da própria base; passado 30% da largura do palco, ele voa para fora, some, e reaparece no fundo da pilha. Não há começo nem fim: a quinta carta é seguida da primeira. Cada card carrega, dentro da própria imagem, um número grande na cor de marca, uma palavra e uma linha.
Resolve a lista curta que não merece uma dobra inteira e não pode virar bullet. Cinco compromissos, cinco princípios, cinco perguntas frequentes de venda — conteúdo que numa grade de cinco caixas fica com cara de tabela de plano, e numa lista com marcador não é lido por ninguém. A pilha compacta os cinco no espaço de um, dá ao visitante um motivo físico para tocar na página, e mantém a coluna de texto livre para argumentar. O preço está declarado nos gotchas: quem não interage lê um card só.
Quando usar
- Cinco itens curtos e paralelos — um substantivo e uma linha cada — que ganham com uma foto por trás e perdem numa grade.
- Quando a dobra precisa de um gesto: página que já tem duas ou três dobras paradas em sequência e pede algo para a mão fazer.
- Quando a ordem não importa. A pilha é circular por construção: quem chega no 03 e sai não perdeu nada.
- Quando existe foto de verdade para os cinco itens, no mesmo tratamento.
Quando não usar
- Quando a ordem importa e todos precisam ser lidos. Aqui o método é
listas/baralho-sticky, e a diferença entre os dois "baralhos" da casa é de natureza, não de estilo: lá a pilha é montada pela rolagem — a página empurra os cards para o topo, um cobrindo o outro, e o visitante percorre os quatro a seis obrigatoriamente, ao custo de uma tela inteira de altura. Aqui a pilha é montada pela mão — a seção tem a altura de uma dobra comum, o gesto é opcional e a fila dá voltas. Um é sequência imposta; o outro é monte que pode ou não ser revirado. Se a promessa é "estas são as etapas, nesta ordem", a pilha mente. - Menos de quatro itens. Com três a pilha não tem degrau para mostrar e a vaga de pouso deixa de existir (ver Gotchas). Três cards de foto com texto por dentro são
conteudo/pilares-texto-na-foto, que é a mesma legenda dentro da imagem, sem gesto. - Quando cada item leva a algum lugar. O card não é link e não pode virar um sem quebrar o arrasto — o clique já é usado pela faixa do card de baixo. Fila de cases clicáveis é
midia/galeria-scroll-horizontal, onde o card é um<a>de verdade. - Quando o assunto é a foto. Aqui a imagem é fundo de um rótulo; ninguém abre a foto, ninguém vê grande. Galeria de verdade é
midia/galeria-pasta. - Quando é gente com biografia.
midia/carrossel-perfismostra três pessoas ao mesmo tempo e guarda a bio atrás do hover; a pilha mostra uma por vez e não comporta parágrafo. - Quando o texto é longo ou precisa ser comparado lado a lado — planos, entidades, perguntas com resposta de cinco linhas:
conteudo/grid-entidadeselistas/acordeao-faq. - Quando os cinco itens são conteúdo obrigatório (obrigação regulatória, promessa contratual). Sem JavaScript, e para quem usa leitor de tela sem operar o carrossel, só o primeiro é lido.
Padrão editorial
| Slot | Regra |
|---|---|
| Olho | 1 palavra, substantivo. O rótulo da categoria, não uma frase. |
Título (h2.sec-title.ttl) |
4 a 7 palavras, com uma em <em>. Diz o que a pilha enumera. Duas linhas na coluna. |
Apoio (.sec-lead) |
2 a 3 linhas. Enquadra o conjunto e carrega o argumento sozinho — é o que quem não tocar na pilha vai ler. |
Número (.swipe-num) |
Dois dígitos, 01…05, na ordem do DOM. Não é decoração: é o que o contador 2 / 5 está dizendo. |
Título do card (.swipe-title) |
Uma palavra, até 14 caracteres. "Independência", "Método", "Presença". Duas palavras já quebram em duas linhas no palco de 320px. |
Linha (.swipe-line) |
Uma frase, até 46 caracteres, sem ponto final. Medido: a 44 caracteres ela ainda cabe em uma linha visual tanto no palco de 420px quanto no de 320px. Acima disso quebra em duas e a mancha da legenda muda de card para card. |
| Quantidade | Sempre uma a mais que --swipe-depth — cinco cards com profundidade 4. Mínimo prático: 4 cards com --swipe-depth:3. |
O que mantém a pilha coesa é o paralelismo gramatical: se um card diz "Sem vínculo com banco ou corretora" (uma ausência), todos dizem o que a empresa não faz ou todos dizem o que ela faz. Como os cinco passam pelo mesmo lugar da tela, com a mesma tipografia e a mesma altura de véu, qualquer variação de forma aparece como defeito e não como variação.
A ordem é editorial. Sem movimento reduzido, sem JavaScript e para quem não toca em nada, o card visível é sempre o 01. Ele tem de ser o compromisso mais forte, não o mais óbvio.
Especificação dos assets
- Formato e dimensão mínima: WebP em retrato, 900×1200 ou maior. O quadro exibido é 4/5 (420×525 no desktop, 320×400 no mobile) com
object-fit:cover— o corte de cada card vem do--swipe-focusnostyledo<li>. - Peso alvo: até 150 KB por foto. As cinco carregam juntas (ver Gotchas), então o orçamento da dobra é o total.
- Composição / enquadramento: assunto nos dois terços de cima. O terço de baixo é ocupado pelo véu da legenda, e um rosto ou um objeto importante ali fica atrás de texto. Fotos de tom fechado e luz baixa: o número em cor de marca precisa de contraste, e cinco fotos com exposições diferentes fazem a pilha piscar a cada troca.
- O que não pode ter: texto, logo ou marca dentro da imagem (a legenda já é texto sobre foto, e duas camadas de tipografia brigam); recorte em fundo branco misturado com foto de ambiente; foto clara demais no terço inferior, onde o véu não tem o que escurecer; moldura ou sombra no próprio arquivo — a borda e a sombra vêm do CSS.
Como usar
<link rel="stylesheet" href="componentes/midia/pilha-swipe/estilo.css">
<script src="componentes/midia/pilha-swipe/script.js"></script>
Cada card declara três coisas no style e nada mais:
<li class="swipe-card" aria-roledescription="slide"
style="--swipe-n:0;--swipe-rot:-2deg;--swipe-focus:50% 42%">
--swipe-n é o degrau inicial (0, 1, 2…, em sequência); depois disso quem escreve é o script. O último card do markup nasce com class="swipe-card is-back", que é a vaga de pouso.
Ganchos
| Gancho | Papel |
|---|---|
[data-swipe] |
A raiz. O script parte daqui e lê nela todas as variáveis de ajuste. |
[data-swipe-stack] |
O <ul>. Cobre o palco inteiro, é o elemento focável, recebe o arrasto e as setas, e carrega aria-roledescription="carrossel". |
[data-swipe-prev] / [data-swipe-next] |
Os dois botões da coluna de texto. |
[data-swipe-count] |
O contador, com aria-live="polite". |
--swipe-n |
Degrau do card na pilha, 0 = topo. Escrito pelo script, lido pelo CSS para posição, escala, inclinação e z-index. É o contrato inteiro entre os dois. |
--swipe-rot |
Inclinação do card, no style do <li>. Só vale do segundo degrau em diante. |
--swipe-focus |
Corte da foto, por card, no style do <li>. Padrão 50% 38%. |
--swipe-fly |
Direção do voo (1 ou -1). Escrito pelo script só enquanto o card está fora. |
--swipe-max |
Largura do palco. 420px; 320px abaixo de 760px. |
--swipe-step |
Faixa que cada card deixa aparecer abaixo do de cima. 12px; 10px abaixo de 420px. |
--swipe-shrink |
Encolhimento por degrau. .045. |
--swipe-depth |
Cards à vista. 4. O markup tem de ter um card a mais que isto. |
--swipe-tail |
Reserva de espaço para a cauda da pilha. Calculada de depth e step — não se mexe. |
--swipe-threshold |
Fração da largura do palco que manda o card embora. .3. |
--swipe-fly-dur / --swipe-back-dur |
Voo e acomodação, em segundos. O script lê as duas para dimensionar o próprio fallback de pouso. |
--swipe-auto |
Troca sozinha, em segundos. 0 = desligada (padrão). |
.is-dragging |
No card sob o dedo: sem transição, z-index alto, will-change. |
.is-out |
No card em voo: 140% para o lado, 22° e opacidade zero. |
.is-instant |
Um quadro sem transição, para teleportar sem desenhar o caminho. |
.is-back |
Card fora da pilha visível: opacity:0 e pointer-events:none. |
Variações
- pilha mais aberta —
--swipe-step:20pxe--swipe-shrink:.06. A faixa de cada card dobra e a pilha lê como leque vertical.--swipe-tailacompanha sozinha. - pilha de seis à vista —
--swipe-depth:5com seis cards no markup. A regra é sempre uma a mais. - avanço automático —
--swipe-auto:6. Medido: com2, o contador anda1 → 2 → 3, para enquanto o foco está dentro e volta a andar ao sair. Abaixo de 4s a linha do card não é lida. Ignorado sob movimento reduzido. - sem inclinação — apague os
--swipe-rotdos<li>. A pilha vira degrau reto; serve quando a dobra vizinha já tem diagonal. - em seção clara —
.sec-paperno lugar de.sec-deep. Botões e contador acompanham o escopo; o card não muda, porque o texto sobre a foto lê--lighte o véu lê--dark-rgb. - texto à direita — inverta
.swipe-copye.swipe-stageno markup. Nada no CSS depende da posição.
Gotchas
-
transform-originfica na BASE do card, e trocar por centro apaga a pilha. Com a origem no centro, o encolhimento come exatamente o deslocamento: num card de 525px,scale(.955)sobe a borda de baixo em 11,8px contra os 12px que otranslateYdesceu. Medido no navegador, com a inclinação zerada para isolar: origem na base dá faixas de0, 12, 24, 36, 48px— exatamenten × --swipe-step; origem no centro dá0, 0.2, 0.4, 0.6, 0.8px, e as cinco fotos leem como uma só. Pior: a conta do centro depende da altura do card, então a pilha que funcionasse num tamanho de tela desmancharia noutro. Ancorada na base, a faixa é--swipe-stepem qualquer largura. -
Não há
perspectiveno palco, de propósito. Nenhum card gira emrotateX/rotateY, eperspectivesobre transformação 2D não muda um pixel — medido: com e semperspective:1200pxotransformcomputado sai idêntico (matrix(0.954673, 0.024999, -0.024999, 0.954673, 0, 12)) e a caixa de todos os cards também. Emmidia/galeria-pastaela é obrigatória porque lá a aba tomba de verdade; aqui seria linha de folha de estilo sem efeito, e alguém acabaria depurando um problema que não existe. -
O quinto card é invisível porque é a vaga de pouso.
--swipe-depth:4com cinco cards: o de índice 4 fica em.is-back(opacity:0). Quando um card voa, os índices são girados na hora — a pilha se acomoda durante o voo — e o card que saiu já vale 4. No fim do voo ele apenas some da posição de fora e reaparece parado nessa vaga invisível, num quadro sem transição. Com--swipe-depthigual ao número de cards, esse teleporte acontece à vista, no meio da pilha. Daí a regra: sempre um card a mais que a profundidade. -
O
overflow:hiddenda seção é estrutural, não faxina. O card voa 140% da própria largura, o que passa da borda da coluna. Medido com o recorte removido, num viewport de 1440px: a página cresce até 1983px durante o voo — 543px de estouro horizontal e barra de rolagem aparecendo e sumindo a cada troca. Com o recorte,overflowXficaokem todos os quadros, no desktop e no mobile. -
O card de cima não se inclina em repouso, e isso não é esquecimento. A fórmula é
rotate(--swipe-rot × min(--swipe-n, 1)): no degrau 0 a inclinação é zerada. O arrasto escreverotate()a partir de zero, e um card parado a −2° daria um salto visível de 2° nopointerdown. Se você quiser o topo inclinado, o arrasto passa a ter de somar a inclinação de repouso — são as duas pontas do mesmo fio. -
O limiar é distância OU velocidade, e as duas precisam existir. Distância: 30% da largura do palco (126px num palco de 420). Velocidade: 0,55 px/ms com pelo menos 14px andados. Medido: um lance de 70px a ~1,75 px/ms descarta o card (é o gesto rápido de quem já entendeu); um empurrão de 60px a 0,5 px/ms devolve o card ao lugar. Só distância obrigaria a arrastar meio card sempre; só velocidade descartaria em qualquer esbarrão.
-
pointercanceldevolve o card, mesmo passado o limiar. No toque,touch-action:pan-ydeixa a rolagem vertical com a página — e quando o navegador assume o gesto, opointerupnunca chega: chegapointercancel. Medido: um arrasto de 200px (bem acima dos 126 do limiar) terminado empointercancelmantém o contador onde estava. Quem estava rolando a página não pediu para descartar nada. Um script que tratepointercancelcomopointerupdescarta um card a cada rolagem feita com o dedo em cima da pilha. -
O script reordena índices; não move nenhum
<li>. A outra forma de fazer a mesma coisa — mandar o card para o fim do<ul>— foi descartada por três motivos: a ordem de leitura do leitor de tela passaria a girar junto (o mesmo bloco seria anunciado03, 04, 05, 01…conforme o uso); reinserir um nó cancela a transição em curso nele; e um nó que muda de pai carregando o foco leva o foco para o<body>em parte dos navegadores. Com índice, a ordem do documento é sempre01…05, o foco fica no<ul>— que nunca se move — e oz-indexsai do número, não da posição no DOM. É o que também permite trocar--swipe-stepou--swipe-shrinksem tocar no script. -
transitionendnão é confiável sozinho. Ele não chega quando a aba está oculta (o navegador congela a transição, que só termina ao voltar) nem quando outra classe corta a transição no meio. Sem rede, o card ficaria fora da tela para sempre e a pilha perderia um item por volta. O script escuta o evento e arma umsetTimeoutdemax(--swipe-fly-dur, --swipe-back-dur) + 220ms; o que chegar primeiro desarma o outro. Se você aumentar as durações no CSS, o fallback acompanha — por isso as duas são lidas de lá e não escritas no script. -
.is-backlevapointer-events:none, e é isso que salva o toque na faixa de baixo.opacity:0continua clicável: sem a guarda, o quinto card — invisível, 48px abaixo do topo — comeria o clique na faixa do quarto. Medido comelementFromPointa partir da base do card de cima: 6px abaixo acerta o cardn=1, 20px acerta on=2, e a 45px não há nada — que é o correto, porque ali só existe o cardn=4, que é o invisível. -
Só o card de cima é anunciado. Os outros levam
aria-hidden="true", como manda o padrão de carrossel, e é por isso que o apoio da coluna da esquerda tem de carregar o argumento sozinho. O mesmo vale sem JavaScript: a pilha nasce montada pelostyleinline, mas nada avança, e quem chega lê um compromisso de cinco. Se os cinco forem conteúdo que a página precisa entregar, este componente é o errado — ver "Quando não usar". -
As cinco fotos carregam juntas, e o
loading="lazy"das quatro de baixo não adia nada. Elas ocupam exatamente a mesma caixa da primeira: quando o palco entra na tela, as cinco entram. O atributo fica porque é a regra da casa, não porque economize — e é bom que seja assim: uma foto que chegasse depois apareceria em branco ao subir para o topo. O orçamento de peso da dobra é a soma das cinco. -
Depurando: ler o
transformcomputado logo depois de escrever--swipe-ndevolve o valor ANTIGO. O card temtransition:transform, entãogetComputedStyle().transformegetBoundingClientRect()entregam o valor animado no instante zero, que ainda é o anterior. Foi o que atrapalhou a primeira medição da faixa da pilha. Para medir geometria de repouso, ponha.is-instantnos cards antes de ler — ou espere a transição terminar. -
O arrasto sempre pega o card de cima, mesmo começando na faixa do de baixo. É deliberado: a faixa visível é alvo de clique (um toque ali avança), e o gesto de puxar pertence ao topo. Um arrasto iniciado na faixa move o card de cima; um clique curto ali, não. Quem separa os dois é o limiar de 6px.
-
Os dois botões são a primitiva
.btn-discdo core, e o markup traz as duas classes (class="btn-disc swipe-btn"). A regra do disco estava copiada ao caractere em cinco pastas —midia/galeria-scroll-horizontal,midia/carrossel-anel-3d,midia/carrossel-foco-central,midia/pilha-swipeemidia/roda-de-cards— e foi promovida aocore/base.cssem 2026-09-03, como o §6 do contrato manda fazer já na terceira. Forma, cor, hover e o estado desabilitado vêm de lá; o parafuso é--disc-size. Não é.btn: aquela é pílula de texto, com padding horizontal fixo.
Histórico
| Projeto | Onde | Data | Diferenças |
|---|---|---|---|
| autoral | construído do zero na biblioteca | 2026-09-03 | — |
Origem. Padrão de interação recorrente em produto premium: a pilha de cards que se descarta com um gesto lateral, conhecida dos aplicativos de relacionamento e de recrutamento, e reaproveitada em vitrines do Awwwards e em componentes de card stack arrastável do ecossistema Framer/Framer Motion. Não há referência única, e nada foi copiado: reescrito do zero dentro do contrato desta biblioteca. O que o padrão de origem quase sempre traz e não atravessa para cá: React, biblioteca de animação com física de mola, e uma pilha que acaba — a versão de aplicativo esvazia a fila porque descartar é a ação; numa dobra de site, esvaziar deixaria um buraco, então aqui a fila dá voltas.
Qual irmão ele substitui, e em que caso (§11 do contrato). Antes de construir, a busca no catálogo deu dois candidatos e nenhum dos dois serve para o caso desta dobra. listas/baralho-sticky empilha cards, mas quem empilha é a rolagem: a sequência é obrigatória e custa uma tela inteira de altura — serve para método em ordem, não para cinco itens sem ordem numa dobra comum. conteudo/pilares-texto-na-foto tem a mesma legenda dentro da foto e resolve três itens lado a lado, parados; com cinco a grade quebra em duas fileiras desiguais e a dobra vira tabela. A pilha-swipe entra exatamente no vão entre os dois: quatro a seis itens curtos, sem ordem, no espaço de um card só e com um gesto para a mão. Fora desse vão, use um dos irmãos.
Decisões de tradução para o contrato.
- Virou token: véu da legenda e sombra da pilha em
rgba(var(--dark-rgb),…); número em--accent; título e linha sobre a foto em--light(ilha escura — o card não muda de cor ao trocar o escopo de tom da seção); canto em--radius-media; disco dos botões em--radius-pill; curva de todo movimento em--ease; tipografia em--displaye--sans. - Virou variável pública: toda a geometria (
--swipe-max,--swipe-step,--swipe-shrink,--swipe-depth,--swipe-tail), o gesto (--swipe-threshold), o tempo (--swipe-fly-dur,--swipe-back-dur) e a troca automática (--swipe-auto). O script lê as seis últimas do CSS e não guarda cópia de nenhuma. - Virou um número só: a posição inteira do card sai de
--swipe-n. É o mesmo canal do--i/--adomidia/galeria-pastae do--deck-idolistas/baralho-sticky, e existe pela mesma razão — mudar o desenho da pilha não pode exigir mexer no JavaScript. - Entrada pelo core:
.ttlno título,data-rv/data-dno olho, no apoio, na navegação e no palco. Nenhum observer próprio. - Acessibilidade que o padrão comum não tem: palco focável com
aria-roledescription="carrossel"e rótulo; setas do teclado,Entere Espaço; dois botões de verdade comaria-label; contador comaria-live="polite";aria-hiddenmantido só nos cards cobertos, sem nenhum elemento focável dentro deles;touch-action:pan-ypara a página continuar rolando no toque vertical; retorno do card empointercancel; e troca instantânea sobprefers-reduced-motion, com o arrasto preservado — gesto pedido pela pessoa não é movimento imposto a ela.
Medições desta entrega (Chrome headless, preview.html): desktop 1440×900 — seção de 813px, palco de 420×525, console limpo, overflowX: ok, cinco fotos pintadas em 900×1200; arrasto de −260px leva o contador a 2 / 5 e ArrowLeft devolve a 1 / 5, sem sobrar nenhuma classe de estado; mobile 390×844 — overflowX: ok, pilha antes do texto, palco de 320×400, 29px de folga entre a cauda da pilha e o topo da coluna de texto, arrasto de −160px avança; movimento reduzido — transition em 0s, troca completa em menos de 60ms e nenhum card em voo. Conformidade: 0 alertas.