Linha do tempo vertical em HTML e CSS: histórico que se lê de relance
Monte uma timeline vertical com lista ordenada, data em texto e uma linha de CSS, sem biblioteca. Funciona no celular, no leitor de tela e com dados vindos da API.
O cliente abre o pedido 4821 e encontra uma tabela com cinco linhas, colunas "evento", "data" e "usuário", em ordem de inserção no banco. Ele só queria saber uma coisa: o pacote já saiu? A resposta curta: uma linha do tempo resolve quando cada marco tem data em texto, um título curto e no máximo uma frase, e quando a ordem é óbvia sem legenda. Em HTML, isso é uma lista ordenada (<ol>) com elementos <time>; a linha vertical é só um detalhe de CSS.
A linha do tempo não é enfeite nem galeria. Ela existe para responder "o que veio antes, o que veio depois e onde estamos agora". Tudo o que compete com essa resposta, como foto grande, ícone em cada marco e animação ao rolar, deixa a página mais bonita e mais lenta de ler.
Por que a tabela e o mural de cards falham
Uma tabela trata cada evento como uma linha igual às outras. A pessoa precisa ler a coluna de data e montar a sequência na cabeça. Um mural de cards em grade é pior: a ordem de leitura vira esquerda para direita, de cima para baixo, e ninguém sabe se o card da direita veio antes ou depois do de baixo.
A linha do tempo vertical funciona porque usa a direção natural da rolagem. O olho desce, a história anda. No celular, que é onde a maioria vai olhar o status de um pedido, não existe espaço para outra coisa. Linhas do tempo horizontais têm seu lugar, mas cobram um preço alto em tela estreita, como discute o post sobre linha do tempo horizontal.
Outro motivo é semântico. Leitor de tela anuncia uma lista ordenada como "lista, 5 itens" e permite pular de item em item. Se a data estiver só num desenho ou numa cor, essa pessoa perde justamente a informação central.
O que cada marco carrega
- Data e hora em texto, no formato que a pessoa usa ("03/03/2026, 10:12"), dentro de
<time datetime>com o valor em formato de máquina. - Título curto, que sozinho já conta o fato: "Pagamento aprovado", "Pedido enviado".
- Uma frase opcional com o detalhe útil: o código de rastreio, a transportadora, o motivo de um atraso.
Foto, botão e ícone são extras. Se todos os marcos têm foto grande, a data some e a página vira galeria. A linha vertical só existe para ligar os pontos. Ela não é o conteúdo.
Como montar a timeline do pedido, passo a passo
- Escolha a ordem e escreva-a no título. Para status, o mais recente em cima, porque a pessoa quer o agora. Para história de empresa ou de carreira, o mais antigo em cima, porque a narrativa começa no início. Escolha uma e diga: "do mais recente para o mais antigo".
- Use uma lista ordenada. Cada marco é um
<li>com título, data e frase. - Marque o passo atual com
aria-current="step". O CSS usa o mesmo atributo para destacar o ponto, então o visual e a acessibilidade nunca divergem. - Desenhe a linha com borda e o ponto com pseudo-elemento. Nenhuma imagem, nenhuma biblioteca.
- Mantenha uma coluna em qualquer largura. Alternar marcos à esquerda e à direita no desktop parece elegante e quebra a ordem de leitura.
O HTML
<section aria-labelledby="historico-titulo">
<h2 id="historico-titulo">Histórico do pedido 4821</h2>
<p>Do mais recente para o mais antigo.</p>
<ol class="timeline">
<li aria-current="step">
<h3>Pedido enviado</h3>
<time datetime="2026-03-04T15:40-03:00">04/03/2026, 15:40</time>
<p>Transportadora Rápida Sul, código de rastreio RS123456789BR.</p>
</li>
<li>
<h3>Nota fiscal emitida</h3>
<time datetime="2026-03-04T09:05-03:00">04/03/2026, 09:05</time>
</li>
<li>
<h3>Pagamento aprovado</h3>
<time datetime="2026-03-03T10:14-03:00">03/03/2026, 10:14</time>
</li>
<li>
<h3>Pedido recebido</h3>
<time datetime="2026-03-03T10:12-03:00">03/03/2026, 10:12</time>
</li>
</ol>
</section>
O CSS
A lista recebe um recuo à esquerda e uma borda, que é a linha. Cada item ganha um ponto posicionado exatamente sobre a borda. A conta do left é simples: o item começa 1,5rem depois da borda de 2px; o ponto tem 14px de largura; para centralizar, você volta 1,5rem, mais 1px (metade da borda), mais 7px (metade do ponto), ou seja, calc(-1.5rem - 8px).
.timeline {
list-style: none;
margin: 0;
padding: 0 0 0 1.5rem;
border-left: 2px solid #c9ced6;
}
.timeline li {
position: relative;
padding: 0 0 1.5rem 1rem;
}
.timeline li::before {
content: "";
position: absolute;
box-sizing: border-box;
left: calc(-1.5rem - 8px);
top: 0.3rem;
width: 14px;
height: 14px;
border-radius: 50%;
border: 2px solid #5b6472;
background: #fff;
}
.timeline li[aria-current="step"]::before {
border-color: #1f6f43;
background: #1f6f43;
}
.timeline h3 {
margin: 0;
font-size: 1rem;
}
.timeline time {
display: block;
color: #4a5160;
font-size: 0.875rem;
}
Confira o contraste do cinza da data contra o fundo. Texto secundário claro demais é o erro mais comum nesse tipo de componente, e ele some de vez no sol, na tela do celular.
Quando os marcos vêm da API
No sistema real, os eventos vêm do servidor, com data em UTC, e o script preenche uma <ol class="timeline"> vazia. Formate no navegador com o fuso da loja e use textContent, nunca innerHTML, para que um texto vindo do banco não vire HTML executável.
const formato = new Intl.DateTimeFormat('pt-BR', {
dateStyle: 'short',
timeStyle: 'short',
timeZone: 'America/Sao_Paulo'
});
function montarTimeline(lista, eventos) {
const ordenados = [...eventos].sort((a, b) => b.em.localeCompare(a.em));
ordenados.forEach((evento, indice) => {
const li = document.createElement('li');
if (indice === 0) li.setAttribute('aria-current', 'step');
const titulo = document.createElement('h3');
titulo.textContent = evento.titulo;
const quando = document.createElement('time');
quando.dateTime = evento.em;
quando.textContent = formato.format(new Date(evento.em));
li.append(titulo, quando);
if (evento.detalhe) {
const p = document.createElement('p');
p.textContent = evento.detalhe;
li.append(p);
}
lista.append(li);
});
}
montarTimeline(document.querySelector('.timeline'), [
{ titulo: 'Pedido recebido', em: '2026-03-03T13:12:00Z' },
{ titulo: 'Pagamento aprovado', em: '2026-03-03T13:14:00Z' },
{ titulo: 'Pedido enviado', em: '2026-03-04T18:40:00Z', detalhe: 'Código de rastreio RS123456789BR.' }
]);
A ordenação por localeCompare funciona porque as datas estão todas em ISO 8601 com o mesmo fuso (Z). Se o servidor misturar formatos, ordene por new Date(...).getTime(). Guardar e transmitir em UTC, e só converter na exibição, evita o marco que aparece "antes" do anterior no horário de verão; o post sobre horário em UTC explica o porquê.
Erros comuns
- Data só no desenho ou na posição. Leitor de tela e quem imprime a página perdem a informação principal.
- Alternar marcos entre esquerda e direita. A ordem de leitura vira zigue-zague, e no celular a alternância nem cabe.
- Linha horizontal com arraste no celular. Os marcos fora da tela ficam invisíveis, e ninguém arrasta para descobrir que existem.
- Foto e ícone em todo marco. A data perde destaque e a página vira galeria.
- Ordem implícita. Sem "do mais recente para o mais antigo" escrito, metade das pessoas lê ao contrário.
- Trazer uma biblioteca de timeline para quatro itens. Você ganha dependência e peso para algo que cabe em 40 linhas de CSS, como discute a segunda biblioteca de timeline.
Checklist
- A timeline é um
<ol>, com um<li>por marco. - Cada marco tem data em texto dentro de
<time datetime>. - A ordem está escrita no título ou logo abaixo dele.
- O passo atual tem
aria-current="step"e destaque visual vindo do mesmo atributo. - Em tela estreita, tudo cabe numa coluna, sem rolagem horizontal.
- Textos vindos da API entram com
textContent. - Datas são guardadas em UTC e formatadas no fuso de quem lê.
Pegue o histórico que hoje é uma tabela no seu sistema e escolha os três eventos que o cliente mais pergunta no suporte. Eles cabem numa timeline com folga, e o mais recente fica em cima porque a pessoa quer o agora, ou embaixo porque a sua história começa no início?
Referência: https://tympanus.net/Blueprints/VerticalTimeline/