Como adicionar uma animação de carregamento a um app ou site
Por equipe Charmotion · Atualizado em

Para adicionar uma animação de carregamento, defina primeiro quanto tempo dura a espera: não mostre nada abaixo de cerca de um segundo, use um spinner ou personagem em loop para esperas de cerca de 2 a 10 segundos e uma barra com a porcentagem concluída acima disso. Depois, exiba a animação só enquanto houver trabalho pendente, anuncie-a com role="status" e respeite as configurações de movimento reduzido.
Principais pontos
- Adapte o indicador à espera: nenhum abaixo de cerca de 1 segundo, um loop para 2 a 10 segundos, uma barra de progresso para 10 segundos ou mais.
- Atrase um pouco a animação de carregamento para que respostas rápidas nunca façam um spinner piscar na tela.
- Anime só
transformeopacitypara que a animação de carregamento continue fluida enquanto a thread principal está ocupada. - Combine o visual com um texto
role="status"earia-busypara que quem usa leitor de tela saiba que algo está acontecendo. - Um personagem da marca funciona como indicador em loop; ele não substitui uma barra de progresso em tarefas longas.
- Para uma animação de carregamento com logo, mantenha o logo parado dentro de um anel girando, ou dê a ele uma pulsação suave em loop perfeito; desative o movimento extra quando o movimento reduzido estiver ativo.
Já tem um personagem ou logo? Veja seu personagem em telas reais de app: telas de splash, onboarding, carregamento e estado vazio. É grátis, roda no navegador e não envia nada.
Spinner, skeleton, barra de progresso ou loader da marca: qual escolher?
Escolha o indicador pela espera prevista e depois pelo contexto. As diretrizes do Nielsen Norman Group definem as faixas de tempo: nenhum indicador para ações abaixo de cerca de um segundo, um indicador em loop, como um spinner, para esperas de cerca de 2 a 10 segundos, e um indicador de porcentagem concluída para qualquer coisa que leve 10 segundos ou mais (NN/g). Essas faixas se baseiam nos limites de tempo de resposta de Nielsen: 0,1 segundo parece instantâneo, 1 segundo mantém o fluxo de raciocínio do usuário e 10 segundos é mais ou menos o limite da atenção.
| Situação | Uso |
|---|---|
| Menos de ~1 segundo | Sem loader. Dê feedback imediato no controle (estado pressionado, botão desativado). |
| Página ou lista cujo layout você já conhece | Um skeleton: placeholders cinza no formato do conteúdo, substituídos no mesmo lugar. |
| 2 a 10 segundos, progresso desconhecido | Um spinner ou um personagem em loop. |
| 10+ segundos, ou trabalho mensurável (uploads, exportações) | Uma barra de progresso com a porcentagem concluída, opcionalmente com um personagem ao lado. |
Skeletons e spinners não são rivais. Um skeleton descreve onde o conteúdo vai aparecer; um spinner ou personagem indica que há trabalho em andamento. Muitas telas usam um skeleton no primeiro carregamento da página e um spinner pequeno para ações posteriores, como "carregar mais". Para os detalhes de tempo por trás dessas faixas, veja duração de animações e UX de carregamento.
Como criar um loader só com CSS?
Um spinner em CSS precisa de um elemento, uma regra de keyframes e nenhum JavaScript. Gire-o com transform: o web.dev recomenda limitar as animações a transform e opacity porque elas ficam na etapa de composição, então o spinner continua girando mesmo enquanto scripts são executados (web.dev).
<div class="loader" role="status">
<span class="spinner" aria-hidden="true"></span>
<span class="visually-hidden">Loading results…</span>
</div>
<style>
.spinner {
display: inline-block;
width: 32px;
height: 32px;
border: 3px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
.spinner { animation-duration: 2.4s; }
}
.visually-hidden {
position: absolute; width: 1px; height: 1px;
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}
</style>
Dê ao spinner largura e altura fixas para que ele nunca desloque o layout ao aparecer. Use currentColor para que ele herde a cor do texto nos temas claro e escuro.
Um skeleton é igualmente simples: blocos com tamanho fixo e uma cor de fundo, opcionalmente com um brilho (shimmer) feito de um gradiente animado movido com transform. Mantenha o brilho sutil e remova-o quando o movimento reduzido estiver ativo.
Como fazer uma animação de carregamento com o seu logo?
Uma animação de carregamento com logo, ou um spinner de carregamento com o seu logo, funciona melhor quando o logo continua legível. Três padrões atendem à maioria das marcas:
- Logo dentro de um anel girando. O logo fica parado e um anel gira ao redor dele. É a opção mais legível e funciona com qualquer formato de logo.
- Uma pulsação suave. O logo diminui alguns por cento e volta ao tamanho original, com uma leve mudança de opacidade. Bom para símbolos compactos e ícones de app.
- O próprio logo gira. Só para marcas redondas e simétricas; um logotipo com texto ou um ícone assimétrico fica difícil de ler e pode causar sensação de tontura.
<div class="logo-loader" role="status">
<img src="/logo-mark.svg" alt="" width="40" height="40">
<span class="visually-hidden">Loading…</span>
</div>
<style>
.logo-loader {
position: relative; display: grid; place-items: center;
width: 64px; height: 64px;
}
.logo-loader::after {
content: ""; position: absolute; inset: 0;
border: 3px solid currentColor; border-right-color: transparent;
border-radius: 50%;
animation: spin 0.9s linear infinite;
}
.logo-loader img { animation: breathe 1.8s ease-in-out infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes breathe { 50% { transform: scale(0.92); opacity: 0.8; } }
@media (prefers-reduced-motion: reduce) {
.logo-loader::after { animation-duration: 2.7s; }
.logo-loader img { animation: none; }
}
</style>
As duas animações usam apenas transform e opacity, e o tamanho fixo reserva o espaço para que nada se desloque. O alt vazio faz os leitores de tela ficarem no texto "Loading…" em vez de anunciarem o logo.
Qual deve ser a duração do loop? Nenhuma diretriz define uma duração de loop. O que importa é que ele seja perfeito, sem emendas (o último frame flui para o primeiro), constante em vez de acelerar e desacelerar, e curto o bastante para se repetir algumas vezes dentro de uma espera típica de 2 a 10 segundos. Nunca deixe uma pulsação virar um pisca-pisca: o WCAG 2.3.1 estabelece um limite de três flashes em qualquer período de um segundo (W3C).
Movimento reduzido: mantenha o indicador e elimine o movimento extra. No exemplo, o anel gira mais devagar e o logo para de pulsar, enquanto o texto de status continua anunciando a espera. Para um loader em tela cheia na abertura do app, as regras são diferentes; veja como transformar o ícone do app em uma intro de abertura.
Como mostrar um personagem da marca durante o carregamento?
Um personagem é um indicador em loop com personalidade, então ele se encaixa na mesma faixa de 2 a 10 segundos que um spinner. Ele funciona melhor em momentos em que o usuário já faz uma pausa: gerar uma imagem, enviar uma mensagem, sincronizar uma biblioteca.
Com um arquivo Rive, o loader é um estado de uma state machine, e não um asset separado. Você define um número para o humor de carregamento enquanto o trabalho é executado e dispara um trigger success ou error quando ele termina, então o mesmo personagem também mostra o resultado. Na web:
import { Rive } from "@rive-app/webgl2";
const r = new Rive({
src: "/character.riv",
canvas: document.querySelector("#mascot"),
stateMachine: "CharacterStateMachine",
autoplay: true,
onLoad: () => {
r.resizeDrawingSurfaceToCanvas();
runWithMascot(); // inputs are available once the file has loaded
},
});
function setMood(n) {
const mood = r.stateMachineInputs("CharacterStateMachine").find((i) => i.name === "mood");
if (mood) mood.value = n;
}
function fire(name) {
r.stateMachineInputs("CharacterStateMachine").find((i) => i.name === name)?.fire();
}
async function runWithMascot() {
setMood(5); // loading loop
await doWork().then(() => fire("success"), () => fire("error"));
setMood(0); // back to idle
}
Os arquivos do Charmotion são feitos exatamente para isso: em um conjunto gerado a partir do seu próprio mascote, o humor 5 é o loop loading (1,2 segundo, sem emendas) e o humor 3 é thinking (2,4 segundos), com os triggers success e error, que voltam ao humor atual. Logos 3D ou com gradiente são animados como estão, sem serem redesenhados, então a mesma abordagem funciona para um loader com logo. Os mesmos estados também ficam disponíveis por meio de um view model Character para Data Binding. Se a tarefa for longa e mensurável, mantenha uma barra de progresso real ao lado do personagem.
Como mostrar um loader só enquanto o trabalho está em andamento?
Vincule o loader à requisição, não a um timer, e adicione um pequeno atraso antes de exibi-lo. Como o usuário não precisa de indicador para ações de menos de cerca de um segundo, um atraso de algumas centenas de milissegundos a um segundo esconde o loader em respostas rápidas e evita um flash que distrai.
async function withLoader(region, task, delay = 600) {
region.setAttribute("aria-busy", "true");
const timer = setTimeout(() => region.classList.add("is-loading"), delay);
try {
return await task();
} finally {
clearTimeout(timer);
region.classList.remove("is-loading");
region.setAttribute("aria-busy", "false");
}
}
O bloco finally é importante: erros e cancelamentos também precisam remover o loader, senão a tela fica girando para sempre. No React, a mesma ideia é um estado pending definido antes da requisição e limpo no finally, com o loader renderizado condicionalmente.
Nas plataformas nativas, os componentes integrados são a escolha padrão. O SwiftUI tem ProgressView() para um spinner indeterminado e ProgressView(value:) para uma barra determinada; o Jetpack Compose tem CircularProgressIndicator e LinearProgressIndicator, com ou sem valor de progresso. Troque por uma view com personagem só onde o momento de marca valer a pena, usando a mesma flag de pendência para mostrá-la e escondê-la.
Como tornar um loader acessível?
Três coisas fazem um loader funcionar para todo mundo:
- Anuncie o carregamento. Coloque um texto curto, como "Carregando resultados…", dentro de um elemento com
role="status", uma live region que os leitores de tela leem de forma educada, sem mover o foco (MDN). Esconda o spinner ou personagem decorativo comaria-hidden="true". - Marque a região como ocupada. Defina
aria-busy="true"no contêiner que está sendo atualizado e volte parafalsequando terminar, para que as tecnologias assistivas possam esperar o conteúdo final. - Respeite o movimento reduzido. A media query
prefers-reduced-motion: reducereflete a configuração de redução de movimento do sistema operacional (MDN). Deixe o spinner mais lento, troque um personagem que pula por uma pose estática com texto ou mostre uma barra de progresso simples. No SwiftUI, leia@Environment(\.accessibilityReduceMotion).
O WCAG 2.2 SC 2.2.2 (Pause, Stop, Hide) normalmente exige uma forma de pausar movimentos que duram mais de cinco segundos junto com outro conteúdo. Ele observa que uma animação durante uma fase de pré-carregamento pode ser considerada essencial quando nenhuma interação é possível naquele momento (W3C). Um loader ao lado de conteúdo que o usuário ainda pode usar não se enquadra nisso, então pare-o quando o trabalho terminar. Saiba mais em animação acessível e movimento reduzido.
Quais são os erros mais comuns em animações de carregamento?
- Um loader para ações instantâneas. Um spinner que aparece por 150 ms parece um bug. Adicione um atraso.
- Um spinner em uma exportação de dois minutos. Depois de cerca de 10 segundos, o usuário quer saber quanto falta. Mostre uma barra com a porcentagem concluída ou uma contagem de passos.
- Bloqueios em tela cheia para atualizações parciais. Se só uma lista está sendo atualizada, coloque o indicador na lista e mantenha o resto da tela utilizável.
- Deslocamento de layout. Um loader que empurra o conteúdo para baixo ao aparecer e depois o puxa de volta é pior do que nenhum. Reserve o espaço dele ou use uma sobreposição.
- Arquivos de loader pesados. Um loader em GIF ou vídeo grande pode demorar mais para baixar do que o próprio trabalho que ele cobre. Um spinner em CSS tem poucas linhas; um arquivo Rive pequeno costuma ter dezenas de kilobytes.
- Estados travados. Sempre remova o loader em caso de erro ou tempo esgotado e substitua-o por uma mensagem clara e uma opção de tentar novamente.
Se o personagem também mostra estados de sucesso e erro, veja estados de animação de mascote para associar eventos a reações.
Como adicionar uma animação de carregamento
- Estime a espera. Meça as durações típicas. Menos de cerca de 1 segundo não precisa de loader; de 2 a 10 segundos pede um indicador em loop; 10 segundos ou mais pede uma barra com a porcentagem concluída.
- Escolha o indicador. Use um skeleton onde o layout é conhecido, um spinner ou um loop de personagem para esperas curtas de duração desconhecida e uma barra de progresso para trabalhos mensuráveis.
- Use propriedades leves. Anime apenas
transformeopacityno CSS, ou use um arquivo Rive pequeno com um humor de carregamento para um personagem. - Vincule à requisição. Mostre o loader após um pequeno atraso enquanto a requisição está pendente e remova-o em um bloco
finallypara que os erros também o limpem. - Torne-o acessível. Adicione um texto com
role="status", alternearia-busy, esconda o movimento decorativo dos leitores de tela e deixe-o mais lento ou substitua-o quando o movimento reduzido estiver ativo.
Perguntas frequentes
Quanto tempo esperar antes de mostrar um spinner de carregamento?
Espere o suficiente para que respostas rápidas nunca cheguem a exibi-lo. O Nielsen Norman Group recomenda não mostrar nenhum indicador em ações que levam menos de cerca de um segundo, então um atraso entre algumas centenas de milissegundos e um segundo funciona bem. Sempre cancele o atraso se a requisição terminar antes.
Skeleton screen é melhor que spinner?
Nenhum dos dois é sempre melhor. O skeleton combina com conteúdo cujo layout você já conhece, como um feed ou uma grade de produtos, porque a página parece se preencher no lugar. O spinner combina com ações sem layout previsível. Muitos apps usam skeleton no primeiro carregamento e spinners depois.
Um mascote pode substituir uma barra de progresso?
Não em tarefas longas. Um mascote em loop mostra que algo está acontecendo, mas não quanto falta. Use-o em esperas de cerca de 2 a 10 segundos e, a partir de 10 segundos, mostre uma barra com a porcentagem concluída, se quiser com o mascote ao lado.
Como fazer um spinner de carregamento com meu logo?
Coloque o logo, parado, no centro de um anel em CSS que gira com transform, ou dê ao próprio logo uma pulsação suave de escala e opacidade. Só gire o logo inteiro se ele for redondo e simétrico. Adicione um texto com role="status", oculte a imagem dos leitores de tela e desacelere ou pare o movimento quando houver preferência por movimento reduzido.
O loader deve parar com movimento reduzido?
Ele deve ficar mais calmo em vez de sumir, porque o usuário ainda precisa saber que algo está em andamento. Desacelere a rotação, troque um personagem que pula por uma pose estática ou mostre uma barra de progresso, e mantenha o texto com role="status" para que o estado continue sendo anunciado.
Respostas rápidas deste guia
Fontes
- Progress Indicators Make a Slow System Less Insufferable, Nielsen Norman Group
- Response Times: The 3 Important Limits, Nielsen Norman Group
- How to create high-performance CSS animations, web.dev
- ARIA: status role, MDN Web Docs
- aria-busy, MDN Web Docs
- prefers-reduced-motion, MDN Web Docs
- Understanding SC 2.2.2: Pause, Stop, Hide, W3C WAI
- Understanding SC 2.3.1: Three Flashes or Below Threshold, W3C WAI
- Web (JS) runtime, Rive
Guias relacionados
- Duração de animações e UX de carregamento
- Estados de animação do mascote: carregamento, sucesso, erro
- Animação acessível: movimento reduzido e WCAG
- Como adicionar um mascote ao onboarding e aos estados vazios
- Como adicionar um logo animado ao cabeçalho do seu site
- Transforme o ícone do seu app em uma intro de abertura