Antes e Depois
Exibe a transformação entre dois estados com animação ou transição. O aprendiz vê o estado inicial e o estado final com destaque para as mudanças, ideal para comparar versões, progressões e melhorias.
Experimente
Email Profissional: Revisão
De: joao@empresa.com
Assunto: reunião
Olá, precisamos marcar uma reunião urgente sobre o projeto que está atrasado. Quando você pode? Responde logo.
3 problemas → 3 melhorias
De: joao@empresa.com
Assunto: Reunião de alinhamento — Projeto Alpha (semana 24)
Bom dia, Maria. Gostaria de agendar 30 minutos para alinharmos o cronograma do Projeto Alpha. Tenho disponibilidade na terça às 14h ou quarta às 10h. Qual horário funciona melhor para você?
Código-fonte
Dados de exemplo
Linhas pareadas por índice — a linha 3 do "antes" corresponde à linha 3 do "depois". O JS usa esse índice pra comparar só as linhas certas entre si.
| 1 |
<?php
|
| 2 |
$antes = [
|
| 3 |
'linhas' => [
|
| 4 |
'Produto: Fone de Ouvido',
|
| 5 |
'Preço: R$ 89,90',
|
| 6 |
'',
|
| 7 |
'Fone de ouvido bom pra usar no dia a dia, com bateria que dura bastante e som de qualidade.',
|
| 8 |
],
|
| 9 |
'problemas' => [
|
| 10 |
'Descrição genérica, sem números concretos',
|
| 11 |
'Não menciona diferenciais em relação aos concorrentes',
|
| 12 |
'Falta um contexto de uso que ajude a pessoa a se identificar',
|
| 13 |
],
|
| 14 |
];
|
| 15 |
$depois = [
|
| 16 |
'linhas' => [
|
| 17 |
'Produto: Fone de Ouvido',
|
| 18 |
'Preço: R$ 89,90',
|
| 19 |
'',
|
| 20 |
'Fone de ouvido com cancelamento de ruído, 30 horas de bateria e som Hi-Fi — ideal para quem trabalha em ambientes barulhentos.',
|
| 21 |
],
|
| 22 |
'melhorias' => [
|
| 23 |
'Números concretos (30 horas, cancelamento de ruído)',
|
| 24 |
'Diferencial técnico específico (som Hi-Fi)',
|
| 25 |
'Contexto de uso que ajuda a pessoa a se identificar',
|
| 26 |
],
|
| 27 |
];
|
| 28 |
?>
|
Estrutura da comparação
As linhas de cada lado recebem data-linha com o mesmo índice — é o que permite ao JS comparar a linha certa do "antes" com a linha certa do "depois", mesmo que a ordem de renderização mude no futuro.
| 1 |
<div class="antes-depois" data-tipo="antes-depois">
|
| 2 |
|
| 3 |
<div class="caixa caixa-antes anima anima-1">
|
| 4 |
<div class="caixa-cabecalho">✕ Versão original</div>
|
| 5 |
<div class="caixa-linhas">
|
| 6 |
<?php foreach ($antes['linhas'] as $i => $linha): ?>
|
| 7 |
<p data-linha="<?= $i ?>"><?= htmlspecialchars($linha) ?></p>
|
| 8 |
<?php endforeach; ?>
|
| 9 |
</div>
|
| 10 |
<div class="caixa-lista">
|
| 11 |
<?php foreach ($antes['problemas'] as $i => $problema): ?>
|
| 12 |
<div class="caixa-item"><span class="numero numero-antes"><?= $i + 1 ?></span> <?= htmlspecialchars($problema) ?></div>
|
| 13 |
<?php endforeach; ?>
|
| 14 |
</div>
|
| 15 |
</div>
|
| 16 |
|
| 17 |
<div class="seta anima anima-2">
|
| 18 |
<span class="seta-icone">↓</span>
|
| 19 |
<p class="seta-resumo"><?= count($antes['problemas']) ?> problemas → <?= count($depois['melhorias']) ?> melhorias</p>
|
| 20 |
</div>
|
| 21 |
|
| 22 |
<div class="caixa caixa-depois anima anima-3">
|
| 23 |
<div class="caixa-cabecalho">✓ Versão revisada</div>
|
| 24 |
<div class="caixa-linhas">
|
| 25 |
<?php foreach ($depois['linhas'] as $i => $linha): ?>
|
| 26 |
<p data-linha="<?= $i ?>"><?= htmlspecialchars($linha) ?></p>
|
| 27 |
<?php endforeach; ?>
|
| 28 |
</div>
|
| 29 |
<div class="caixa-lista">
|
| 30 |
<?php foreach ($depois['melhorias'] as $i => $melhoria): ?>
|
| 31 |
<div class="caixa-item"><span class="numero numero-depois"><?= $i + 1 ?></span> <?= htmlspecialchars($melhoria) ?></div>
|
| 32 |
<?php endforeach; ?>
|
| 33 |
</div>
|
| 34 |
</div>
|
| 35 |
|
| 36 |
</div>
|
Caixas antes/depois
Mesma estrutura visual pros dois lados — só a cor muda (vermelho pro "antes", verde pro "depois").
| 1 |
.caixa {
|
| 2 |
border-radius: 12px;
|
| 3 |
overflow: hidden;
|
| 4 |
font-family: inherit;
|
| 5 |
}
|
| 6 |
.caixa-antes {
|
| 7 |
border: 1px solid rgba(239, 68, 68, .3);
|
| 8 |
background: rgba(127, 29, 29, .05);
|
| 9 |
}
|
| 10 |
.caixa-depois {
|
| 11 |
border: 1px solid rgba(34, 197, 94, .3);
|
| 12 |
background: rgba(20, 83, 45, .05);
|
| 13 |
}
|
| 14 |
.caixa-cabecalho {
|
| 15 |
padding: 8px 16px;
|
| 16 |
font-size: 13px;
|
| 17 |
font-weight: 700;
|
| 18 |
}
|
| 19 |
.caixa-antes .caixa-cabecalho {
|
| 20 |
background: rgba(127, 29, 29, .15);
|
| 21 |
color: #dc2626;
|
| 22 |
}
|
| 23 |
.caixa-depois .caixa-cabecalho {
|
| 24 |
background: rgba(20, 83, 45, .15);
|
| 25 |
color: #16a34a;
|
| 26 |
}
|
Linhas de texto
Fonte monoespaçada — o conteúdo comparado costuma ser texto "de verdade" (e-mail, descrição, trecho de código), então a fonte de código deixa a comparação mais clara que uma fonte de leitura corrida.
| 1 |
.caixa-linhas {
|
| 2 |
padding: 12px 16px;
|
| 3 |
font-family: 'Courier New', monospace;
|
| 4 |
font-size: 12px;
|
| 5 |
line-height: 1.6;
|
| 6 |
}
|
| 7 |
.caixa-linhas p {
|
| 8 |
margin: 0;
|
| 9 |
}
|
Destaque de palavra
Calculado pelo JS (diffPalavras) — fundo fraco, sem risco. Um risco sobre uma frase inteira (não uma palavra solta) atrapalha a leitura corrida da comparação.
| 1 |
.palavra-removida {
|
| 2 |
background: rgba(239, 68, 68, .18);
|
| 3 |
border-radius: 3px;
|
| 4 |
padding: 0 2px;
|
| 5 |
}
|
| 6 |
.palavra-adicionada {
|
| 7 |
background: rgba(34, 197, 94, .2);
|
| 8 |
border-radius: 3px;
|
| 9 |
padding: 0 2px;
|
| 10 |
font-weight: 600;
|
| 11 |
}
|
Lista de problemas/melhorias
O número em cada item deixa explícito que o problema N do "antes" foi resolvido pela melhoria N do "depois" — sem isso, as duas listas ficam soltas, sem uma ligação visível entre elas.
| 1 |
.caixa-lista {
|
| 2 |
padding: 8px 16px 16px;
|
| 3 |
border-top: 1px solid rgba(0, 0, 0, .08);
|
| 4 |
font-size: 12px;
|
| 5 |
}
|
| 6 |
.caixa-item {
|
| 7 |
display: flex;
|
| 8 |
align-items: center;
|
| 9 |
gap: 8px;
|
| 10 |
padding: 3px 0;
|
| 11 |
}
|
| 12 |
.caixa-antes .caixa-item { color: #dc2626; }
|
| 13 |
.caixa-depois .caixa-item { color: #16a34a; }
|
| 14 |
.numero {
|
| 15 |
flex-shrink: 0;
|
| 16 |
width: 16px;
|
| 17 |
height: 16px;
|
| 18 |
border-radius: 999px;
|
| 19 |
font-size: 10px;
|
| 20 |
font-weight: 700;
|
| 21 |
display: flex;
|
| 22 |
align-items: center;
|
| 23 |
justify-content: center;
|
| 24 |
}
|
| 25 |
.numero-antes { background: rgba(239, 68, 68, .15); color: #dc2626; }
|
| 26 |
.numero-depois { background: rgba(34, 197, 94, .15); color: #16a34a; }
|
Seta central
Além de indicar "desça o olho", mostra um resumo em números do que mudou.
| 1 |
.seta {
|
| 2 |
display: flex;
|
| 3 |
flex-direction: column;
|
| 4 |
align-items: center;
|
| 5 |
gap: 2px;
|
| 6 |
margin: 8px 0;
|
| 7 |
}
|
| 8 |
.seta-icone {
|
| 9 |
font-size: 20px;
|
| 10 |
color: #6366f1;
|
| 11 |
}
|
| 12 |
.seta-resumo {
|
| 13 |
margin: 2px 0 0;
|
| 14 |
font-size: 11px;
|
| 15 |
color: #64748b;
|
| 16 |
}
|
Animação de entrada
As três partes (antes, seta, depois) entram em sequência — reforça a narrativa "uma coisa leva à outra". Respeita prefers-reduced-motion.
| 1 |
@keyframes entrada {
|
| 2 |
from { opacity: 0; transform: translateY(8px); }
|
| 3 |
to { opacity: 1; transform: translateY(0); }
|
| 4 |
}
|
| 5 |
.anima {
|
| 6 |
animation: entrada .45s ease-out both;
|
| 7 |
}
|
| 8 |
.anima-1 { animation-delay: 0s; }
|
| 9 |
.anima-2 { animation-delay: .15s; }
|
| 10 |
.anima-3 { animation-delay: .3s; }
|
| 11 |
@media (prefers-reduced-motion: reduce) {
|
| 12 |
.anima { animation: none; }
|
| 13 |
}
|
Diff por palavra
Maior subsequência comum (LCS) clássica entre as palavras de duas linhas — acha o que se manteve, o que saiu (só existe em "a") e o que entrou (só existe em "b"). Roda em cima do texto que já está na página, sem precisar de anotação manual no conteúdo.
| 1 |
document.addEventListener('DOMContentLoaded', function () {
|
| 2 |
function tokenizar(texto) {
|
| 3 |
return texto.split(/(\s+)/).filter(function (t) { return t !== ''; });
|
| 4 |
}
|
| 5 |
|
| 6 |
function diffPalavras(a, b) {
|
| 7 |
var m = a.length, n = b.length;
|
| 8 |
var dp = [];
|
| 9 |
for (var i = 0; i <= m; i++) dp.push(new Array(n + 1).fill(0));
|
| 10 |
for (var i = 1; i <= m; i++) {
|
| 11 |
for (var j = 1; j <= n; j++) {
|
| 12 |
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]);
|
| 13 |
}
|
| 14 |
}
|
| 15 |
|
| 16 |
var i = m, j = n;
|
| 17 |
var resultA = [], resultB = [];
|
| 18 |
while (i > 0 && j > 0) {
|
| 19 |
if (a[i - 1] === b[j - 1]) {
|
| 20 |
resultA.unshift({ texto: a[i - 1], tipo: 'igual' });
|
| 21 |
resultB.unshift({ texto: b[j - 1], tipo: 'igual' });
|
| 22 |
i--; j--;
|
| 23 |
} else if (dp[i - 1][j] >= dp[i][j - 1]) {
|
| 24 |
resultA.unshift({ texto: a[i - 1], tipo: 'removido' });
|
| 25 |
i--;
|
| 26 |
} else {
|
| 27 |
resultB.unshift({ texto: b[j - 1], tipo: 'adicionado' });
|
| 28 |
j--;
|
| 29 |
}
|
| 30 |
}
|
| 31 |
while (i > 0) { resultA.unshift({ texto: a[i - 1], tipo: 'removido' }); i--; }
|
| 32 |
while (j > 0) { resultB.unshift({ texto: b[j - 1], tipo: 'adicionado' }); j--; }
|
| 33 |
return { a: resultA, b: resultB };
|
| 34 |
}
|
| 35 |
|
| 36 |
function escaparHtml(texto) {
|
| 37 |
var div = document.createElement('div');
|
| 38 |
div.textContent = texto;
|
| 39 |
return div.innerHTML;
|
| 40 |
}
|
Aplicar o diff nas linhas correspondentes
Cada linha do "antes" procura, pelo mesmo data-linha, a linha equivalente no "depois". Só marca quando as duas têm texto e são diferentes — linhas em branco (espaçadoras) ou idênticas ficam como estão.
| 1 |
document.querySelectorAll('[data-tipo="antes-depois"]').forEach(function (el) {
|
| 2 |
el.querySelectorAll('.caixa-antes .caixa-linhas p').forEach(function (linhaAntes) {
|
| 3 |
var linhaDepois = el.querySelector('.caixa-depois .caixa-linhas p[data-linha="' + linhaAntes.dataset.linha + '"]');
|
| 4 |
if (!linhaDepois) return;
|
| 5 |
|
| 6 |
var textoAntes = linhaAntes.textContent;
|
| 7 |
var textoDepois = linhaDepois.textContent;
|
| 8 |
if (!textoAntes.trim() || !textoDepois.trim() || textoAntes === textoDepois) return;
|
| 9 |
|
| 10 |
var diff = diffPalavras(tokenizar(textoAntes), tokenizar(textoDepois));
|
| 11 |
linhaAntes.innerHTML = diff.a.map(function (p) { return p.tipo === 'igual' ? escaparHtml(p.texto) : '<span class="palavra-removida">' + escaparHtml(p.texto) + '</span>'; }).join('');
|
| 12 |
linhaDepois.innerHTML = diff.b.map(function (p) { return p.tipo === 'igual' ? escaparHtml(p.texto) : '<span class="palavra-adicionada">' + escaparHtml(p.texto) + '</span>'; }).join('');
|
| 13 |
});
|
| 14 |
});
|
| 15 |
});
|