Duas fotos do mesmo enquadramento, uma sobre a outra, e um divisor vertical que a pessoa arrasta para revelar uma ou outra. A de cima é recortada por clip-path; a de baixo está sempre inteira ali atrás.
Uma fileira opcional de miniaturas embaixo troca o par comparado — de um a quatro pares na mesma dobra. Com um par só, a fileira não existe no markup e o componente é exatamente o que sempre foi.
O que ele resolve: mostrar transformação sem pedir que a pessoa compare duas imagens lado a lado na cabeça. Colocadas uma sobre a outra, a diferença aparece no mesmo pixel — e o gesto de revelar é o que faz a pessoa parar na dobra.
Compliance
Antes e depois é exatamente onde um site de assessoria escorrega para promessa de resultado. A peça é convincente justamente porque parece prova: duas imagens, o mesmo enquadramento, a diferença no mesmo pixel. Se o que está de um lado e do outro for dinheiro, a peça inteira vira insinuação de rentabilidade — e ela insinua melhor do que qualquer frase, porque não diz nada e mostra tudo.
Pode comparar:
- organização — extratos espalhados em cinco instituições e um relatório só;
- clareza de relatório — a planilha herdada e a página que a substituiu;
- antes e depois de uma consolidação — contas, custódias, documentos;
- estrutura documental — a sucessão sem plano e com plano, a governança sem agenda e com agenda.
Não pode comparar:
- patrimônio — "R$ 2 mi" de um lado e "R$ 3,4 mi" do outro é promessa de resultado com cara de foto;
- rentabilidade, em número, gráfico ou curva;
- resultado de carteira em qualquer forma, inclusive "a carteira antes" e "a carteira depois" com posições e valores visíveis;
- comparação com índice, com concorrente ou com "o que você teria se".
A regra é a mesma que conteudo/trio-cards-ui-simulada e conteudo/lista-beneficios-palco declaram: estoque e estrutura, nunca resultado. Aqui ela é mais dura, porque a forma da peça é comparativa por construção.
A ressalva é slot exigido, não letra miúda opcional. O .cmp-nota embaixo do quadro veste a primitiva .nota do core e diz três coisas: o que está sendo comparado, o que não está, e a natureza das imagens. Se as fotos forem geradas por IA, ilustrativas ou telas simuladas, isso se declara ali — uma tela de relatório inventada é a forma mais convincente de parecer um extrato de cliente.
O texto do markup é um ponto de partida real, não um lorem: adapte, não apague.
Quando usar
- Consolidação, reorganização, "como fica depois que a gente entra" — o caso da casa.
- Antes e depois de um relatório, de um documento, de um plano — quando a mudança é de forma e cabe em duas imagens do mesmo ponto de vista.
- Duas a quatro frentes do mesmo argumento, cada uma com o seu par: consolidação, relatório, sucessão, governança. É para isso que a fileira existe.
- Reforma, obra, paisagismo, restauro — o caso clássico, fora do assunto financeiro.
Quando não usar
- Quando as duas imagens têm enquadramentos diferentes. O componente sobrepõe, não justapõe: recortes diferentes fazem o divisor parecer quebrado. Use uma grade de duas colunas.
- Quando a diferença é de detalhe pequeno. A pessoa não vai encontrar. Um par de closes lado a lado com seta serve melhor.
- Mais de dois estados no MESMO par. A fileira dá vários pares de dois estados, não três estados de um par. Para três ou mais estágios de uma coisa só, é
dados/comparador-cenarios. - Mais de quatro pares. A fileira embrulha para uma segunda linha e a dobra passa a pedir uma galeria, não um comparador.
O irmão mais próximo, e quando ele ganha: dados/comparador-cenarios. Ele compara números em cenários nomeados, com eixo e legenda; este compara imagens. Quando o argumento é quanto muda, é o irmão; quando é como fica, é este. E há um caso em que o irmão ganha mesmo sendo sobre imagem: quando os dois estados precisam ser vistos ao mesmo tempo para valer — o divisor sempre esconde metade.
Padrão editorial
| Slot | Onde | Medida | Regra |
|---|---|---|---|
| olho | .eyebrow |
2–4 palavras | |
| título da seção | h2.sec-title |
3–6 palavras | uma palavra em <em> |
| apoio | .sec-lead |
1–2 frases | a legenda real — o que aconteceu entre uma foto e a outra. Com fileira, diga que há mais de um par e que a pessoa escolhe |
| rótulo de cima | .cmp-rotulo-a |
1–2 palavras | o estado que se deve ver primeiro |
| rótulo de baixo | .cmp-rotulo-b |
1–2 palavras | o par do de cima |
| ressalva | .cmp-nota |
1–2 frases | exigido — ver Compliance |
| nome do par | .cmp-par-nome |
1–2 palavras | por par, dentro da miniatura |
| rótulo de cima do par | data-cmp-rot-a |
1–2 palavras | por par |
| rótulo de baixo do par | data-cmp-rot-b |
1–2 palavras | por par |
| descrição da foto do par | data-cmp-alt |
4–16 palavras | por par — o alt da foto de baixo |
Os rótulos são duas palavras, e só. Antes e Depois é o padrão; 2019 e Hoje funciona; Espalhado e Consolidado funciona. Frase não cabe — a pílula fica sobre a foto e compete com ela.
A ordem importa. O que está por cima (recortado) é o estado que a pessoa deve ver primeiro, porque o divisor começa no meio e o olho vai para a esquerda. Se o argumento é o depois, considere a variação começa fechado: a dobra abre mostrando quase só o antes, e o gesto vira convite.
Com fileira, os rótulos visíveis são escritos pelo script. A copy de cada par vive em data-cmp-rot-a e data-cmp-rot-b na miniatura; quem escrever só dentro de .cmp-rotulo-a vê o texto sumir na primeira troca. Mesmo precedente de midia/roda-de-cards.
O nome da miniatura nomeia o CASO, não a ordem. É ele que o leitor de tela anuncia na troca: Relatório — par 2 de 4. Par 2 não diz nada a quem não vê a foto.
Especificação dos assets
Duas fotos por par, 1600×900 ou maior, e a exigência que define o componente: mesmo enquadramento. Mesma posição de câmera, mesma distância focal, mesmo recorte. Um grau de diferença já aparece como fantasma na linha do divisor.
Na prática isso quer dizer tripé, ou pelo menos uma marca no chão. Se as fotos já existem e não batem, o alinhamento se resolve no recorte — mas se resolve antes de chegar aqui, não com CSS.
A miniatura pode ser a própria foto "depois" do par: o CSS recorta em 16:9 e escurece as inativas. Um arquivo menor e dedicado é melhor para o peso da página, mas não é obrigatório.
No preview, o "antes" é a mesma foto com filtro (grayscale, contraste e brilho reduzidos), porque a biblioteca não tem um par de fotos do mesmo enquadramento em showroom/demo/ e usar duas fotos diferentes faria o componente parecer quebrado em vez de comparar. O filtro é regra do elemento, não do par: ele vale para todos, porque trocar de par troca o src e não a classe. No uso real, esse filtro sai e entram os dois arquivos de cada par.
Como usar
Cole a seção inteira. Troque os src, os rótulos, os nomes das miniaturas, os alt — e a ressalva.
Para um par só, apague o bloco .cmp-pares inteiro. O script procura a fileira, não encontra e sai calado; divisor, teclado e arraste continuam idênticos. A .nota fica: ela é exigida com um par ou com quatro.
Para dois ou três pares, apague as miniaturas que sobram. A fileira é flex com base de 140px e se reacomoda sozinha — não há media query por contagem.
O alt da foto de cima é vazio de propósito, e o .cmp-topo inteiro é aria-hidden: as duas imagens mostram a mesma cena, e descrever as duas faria o leitor de tela anunciar a mesma coisa duas vezes. A descrição vai na de baixo, e é ela que o data-cmp-alt de cada par substitui.
Ganchos
| Gancho | O quê |
|---|---|
[data-cmp] |
a raiz de cada quadro. Pode repetir na página |
[data-cmp-range] |
o <input type="range"> que comanda, dentro do quadro |
[data-cmp-conjunto] |
o invólucro que amarra a fileira ao quadro. Sem ele, o segundo comparador da página obedeceria às miniaturas do primeiro |
[data-cmp-img-a] / [data-cmp-img-b] |
as duas fotos grandes — a de cima (antes) e a de baixo (depois) |
[data-cmp-pares] |
a fileira de miniaturas. Opcional |
[data-cmp-par] |
cada miniatura, com data-cmp-src-a, -src-b, -rot-a, -rot-b, -alt |
[data-cmp-vivo] |
a região viva (role="status") que anuncia o par novo |
Parafusos
| Parafuso | O quê |
|---|---|
--cmp-k |
0 a 100, escrita pelo script no quadro. O CSS lê no clip-path e na posição da linha |
--cmp-alca-size |
diâmetro da alça. 46px, 40px no mobile |
--nota-size, --nota-max |
do core, na .nota. Só mexa se a coluna do cliente for muito estreita |
Variações
| Variação | Como |
|---|---|
| Um par só | apagar o bloco .cmp-pares. O componente volta a ser o de antes de 2026-09-21 |
| Dois ou três pares | apagar as miniaturas que sobram |
| Divisor horizontal | clip-path: inset(0 0 calc(100% - var(--cmp-k)*1%) 0) e a linha em inset-block-start. Bom para foto aérea ou fachada |
| Sem rótulos | apagar as duas .cmp-rotulo. Com fileira, apagar também os data-cmp-rot-* — o script só escreve no rótulo que existe |
| Começa fechado | value="8" no range. A troca de par devolve o divisor a esse valor, não a 50 |
| Alça maior | --cmp-alca-size: 58px. Em dobra de tela cheia a alça de 46px some |
| Fileira ao lado (experimental) | grade de duas colunas com o quadro numa e as miniaturas empilhadas na outra. Cabe quando a dobra é larga e o quadro é retrato |
Gotchas
-
O controle é um
<input type="range">nativo, invisível sobre a imagem. É a decisão que define o componente. Quase toda implementação deste padrão escreve o arraste à mão compointerdown/pointermove/pointerup, e aí precisa reimplementar teclado, toque, captura de ponteiro e anúncio para leitor de tela — quatro coisas que o range já traz prontas. A fileira de pares não encosta em nenhuma delas: ela trocasrc, rótulo ealt, e devolve o range à posição inicial. Nada do teclado passa pelo script. -
As miniaturas são
<button>comaria-pressed, não abas. Aba (role="tab") promete um painel por aba e um laço de setas que anda entre elas. Aqui há um painel só, que muda de conteúdo — e as setas do teclado já pertencem ao range. Dar setas às miniaturas roubaria o gesto principal de quem navega por teclado, que é justamente o gesto que o componente existe para oferecer. -
A troca devolve o divisor à posição inicial, e lê o
defaultValueem vez de cravar 50. A posição em que a pessoa largou o par anterior não significa nada no par novo: 38% ali era "quase tudo antes" de uma foto que já saiu de cena. Ler o atributovaluedo HTML é o que faz a variação começa fechado continuar valendo depois da troca. -
O pisca branco da primeira troca, e por que não se resolve baixando tudo. Trocar o
srcpara uma foto que ainda não chegou pinta o quadro de branco por alguns quadros. Pré-carregar os quatro pares no load resolveria — e faria a dobra baixar oito arquivos grandes para quem talvez nunca clique. A saída: as miniaturas sãoloading="lazy", as fotos grandes dos pares fora de cena não estão no DOM, e o script as pede na primeira intenção (ponteiro em cima ou foco na miniatura), trocando osrcsó quando a nova está decodificada. Quem clica direto, sem passar o ponteiro, também não vê pisca: a troca espera. Medido: 211 quadros vigiados numa troca, 0 comsrcnovo e imagem não pintada. -
O estado responde antes da imagem, de propósito. O
aria-pressede o contorno andam para a miniatura clicada no mesmo tique do clique; a foto chega quando chegar. Quem clicou tem resposta imediata mesmo com a rede ruim. Um contador de pedido impede que o par do primeiro clique chegue por último e vença. -
clip-path, nãowidth. Recortar mantém a foto de cima parada enquanto o divisor anda. Comwidth, a imagem seria remontada a cada quadro e pareceria esticar em vez de ser revelada — e o efeito inteiro se perde. -
touch-action: pan-yno quadro. Sem isso, arrastar sobre a imagem no celular rola a página em vez de mover o divisor. Comnoneseria pior: prenderia a página inteira para quem só queria passar por ela. -
A linha e a alça são
pointer-events:none. Elas são desenho; quem recebe o gesto é o range que está por cima. -
O foco aparece na alça, não no range. O range é invisível, e foco em elemento invisível não se vê. O anel vem por seletor irmão (
.cmp-range:focus-visible ~ .cmp-linha .cmp-alca). As miniaturas têm o anel delas, direto no botão. -
O polegar do range tem
block-size: 100vhde propósito. Ele é invisível, e essa altura é o que faz o arraste começar de qualquer ponto da foto, não só de uma faixa central. É um valor absurdo à vista, e proposital. -
O
aria-labeldo range acompanha o par. Um slider que continuasse dizendo "antes e depois" depois de a peça virar "sem plano / com plano" mentiria para quem só ouve. O script reescreve o rótulo a cada troca, a partir dos doisdata-cmp-rot-*. -
Sem JavaScript, a fileira some inteira (
@media (scripting: none)). Botão que não troca nada é pior que botão ausente — e o comparador do primeiro par continua ali, legível, com a ressalva embaixo. A regra[hidden]está declarada à mão, porque quem dádisplaydesliga o[hidden]do navegador (§6.5). -
As duas fotos de cada par precisam do mesmo enquadramento. Não é preferência: um grau de diferença aparece como fantasma na linha.
-
aria-hiddenno.cmp-topoealt=""na foto de cima. As duas imagens são a mesma cena; descrever as duas faria o leitor de tela anunciar duas vezes.
Movimento reduzido / toque
Com prefers-reduced-motion: reduce, o divisor continua andando: ele responde ao gesto, e gesto é comando, não enfeite. Saem a suavização de 90ms entre um passo de teclado e o seguinte e o acender/apagar das miniaturas — os dois únicos movimentos que a pessoa não pediu. Medido: transition-duration 0s no .cmp-topo e no .cmp-par, com o divisor ainda respondendo à seta (50 → 50,1).
No toque, o arraste vem do range nativo, e touch-action: pan-y garante que a rolagem vertical da página continue funcionando. A alça é desenhada sempre, e não só no hover, justamente porque em toque não há cursor ew-resize para avisar do gesto. No celular a fileira faz duas miniaturas por linha (170×97 medidos a 390px) — alvo de toque folgado, sem overflow horizontal.
Por teclado: Tab chega ao range, setas movem de 0,1 em 0,1, PageUp/PageDown movem em passos maiores, Home e End vão às pontas. Depois do range, Tab percorre as miniaturas, e Enter ou Espaço troca o par. Nada disso foi escrito à mão.
Histórico
| Origem | Quando | O quê |
|---|---|---|
| autoral | 2026-09-04 | Construído do zero. Padrão de domínio comum, sem referência única |
| referência: skinboost-design-review.vercel.app | 2026-09-21 | A fileira de pares, a ressalva .nota exigida e a voz de assessoria. estavel |
Origem. O comparador com divisor arrastável aparece em página de reforma, de retoque, de antes e depois de produto e em toda ferramenta de comparação de imagem — padrão de domínio comum, sem dono. Nada foi copiado: reescrito do zero dentro do contrato desta biblioteca. O registro consolidado vai em projetos/autorais.md; a evolução de 2026-09-21 em projetos/referencias-externas.md.
Decisões da construção. A principal está no primeiro gotcha: o range nativo no lugar do arraste escrito à mão. Além dela: clip-path em vez de width, para a foto de cima ficar parada; touch-action: pan-y; linha e alça em pointer-events:none; e o anel de foco na alça desenhada, por seletor irmão a partir do range invisível.
O que 2026-09-21 trouxe. A dobra #evolucao da referência põe uma fileira de rostos sobre o comparador: cada rosto é um caso, e escolher um troca o par sem sair da dobra. Atravessou o que o olho faz — a fileira, a troca do par inteiro de uma vez, o divisor recomeçando do meio. Não atravessou nada mais: o original move a troca com GSAP, monta as miniaturas com componente de terceiro e usa retratos de pessoas reais. Aqui a troca é uma atribuição de src e três textContent. Entraram por decisão nossa: os <button> com aria-pressed em vez de abas, a volta ao defaultValue, o pré-carregamento por intenção (medido: 0 quadros em branco em 211), a fileira que some sem JavaScript, o aria-label do range que acompanha o par — e a ressalva .nota como slot exigido, porque a peça mudou de assunto e antes/depois de dinheiro é promessa de resultado.
Calibragem no primeiro uso. Espere ajustar o value inicial do range — meio-a-meio é o padrão, mas quando o depois é o argumento, começar fechado costuma converter melhor. Espere ajustar o aspect-ratio: 16:9 serve a paisagem, e uma fachada quase sempre pede 4:3 ou 3:4. E espere escrever a ressalva antes das fotos: é ela que decide se o par que você escolheu pode ou não estar nesta peça.