Tema
BaseSkeleton
Ocupa o lugar do conteúdo enquanto ele carrega, com a forma do que vai aparecer. Evita que a tela pule quando os dados chegam.
Uso
- Use no primeiro carregamento de uma área: lista, tabela, cartão, painel.
- Coloque o esqueleto sobre uma superfície
--tj-layer-01(cartão ou painel). Direto no fundo da página (--tj-background), a forma fica quase invisível. - Desenhe a forma do conteúdo real: linhas para texto, retângulo para imagem ou gráfico, círculo para avatar.
- Para uma ação do usuário, como salvar, use o
loadingdo botão, não o esqueleto. - O brilho roda três vezes (4,2 s) e para; a forma continua parada no lugar. Se a espera passar de 5 s, troque o esqueleto por um texto visível na tela, como "Os contratos estão demorando para carregar", não só pelo status oculto.
- Quando os dados chegam, troque o esqueleto pelo conteúdo de uma vez, sem animação de saída.
Variantes
| Variante | Quando | Medidas |
|---|---|---|
text | Títulos e parágrafos | lines linhas, de 1 a 20; a última tem 60% da largura |
rect | Imagens, gráficos, campos, cartões | width e height |
circle | Avatares e ícones redondos | width é o diâmetro; continua redondo numa linha flex |
Tamanhos
O size acompanha o texto que a linha substitui: sm para 13 px (o texto small), md para 14 px, lg para 16 px. Um bloco de N linhas tem a altura de N linhas do texto real (18, 20 ou 24 px cada), então nada pula quando o conteúdo chega. No rect e no circle, ele dá a medida padrão (32, 40 ou 48 px) quando height ou width não são informados.
Estilo
| Propriedade | Token |
|---|---|
| Fundo | --tj-layer-accent-01, sobre --tj-layer-01 |
| Brilho | --tj-layer-hover-01; no tema escuro, --tj-layer-accent-02 |
| Raio | --tj-radius-sm; circle é redondo |
| Altura da linha | --tj-text-sm/md/lg (13, 14, 16 px) |
| Espaço entre linhas e nas bordas do bloco | --tj-leading-* menos --tj-text-*; metade disso acima da primeira e abaixo da última linha |
Medida padrão de rect e circle | --tj-control-sm/md/lg (32, 40, 48 px) |
| Animação | 1,4 s por ciclo, três ciclos, como no protótipo; --tj-easing-standard |
No tema escuro os tokens de camada trocam sozinhos, e o brilho usa --tj-layer-accent-02 para continuar mais claro que o fundo, como no tema claro. Com prefers-reduced-motion, o brilho não roda e a forma fica parada. No modo de alto contraste do sistema, cada forma ganha contorno.
Código
vue
<script setup lang="ts">
import { onMounted, ref } from 'vue'
const loading = ref(true)
const statusText = ref('')
const contracts = ref<Contract[]>([])
onMounted(async () => {
// The status region is already on the page; only its text changes.
statusText.value = 'Carregando contratos'
try {
contracts.value = await fetchContracts()
statusText.value = 'Contratos carregados'
} catch {
statusText.value = 'Não deu para carregar os contratos'
} finally {
// Success or failure, the area stops being busy.
loading.value = false
}
})
</script>
<template>
<p class="visually-hidden" role="status">{{ statusText }}</p>
<section :aria-busy="loading || undefined" aria-labelledby="contracts-title">
<h2 id="contracts-title">Contratos</h2>
<template v-if="loading">
<BaseSkeleton :lines="3" />
<BaseSkeleton variant="rect" height="160px" />
</template>
<ContractList v-else :contracts="contracts" />
</section>
</template>| Propriedade | Tipo | Padrão | Descrição |
|---|---|---|---|
variant | 'text' | 'rect' | 'circle' | 'text' | Forma do conteúdo que vai chegar |
size | 'sm' | 'md' | 'lg' | 'md' | Altura da linha no text; medida padrão no rect e no circle |
lines | number | 1 | Número de linhas do text, de 1 a 20; valores fora disso são ajustados ao limite |
width | string | 100%; no circle, o size | Qualquer medida CSS, como '60%' ou '120px' |
height | string | o size | Altura do rect, em qualquer medida CSS; ignorada no text e no circle |
Sem eventos nem slots.
Acessibilidade
- As formas são decorativas: recebem
aria-hidden="true"e não chegam ao leitor de tela. - Quem avisa que algo está carregando é o contêiner da área, não o esqueleto:
- marque a área com
aria-busy="true"enquanto carrega e tire o atributo quando o conteúdo chegar (:aria-busy="loading || undefined"; comfalse, o Vue deixariaaria-busy="false"no HTML); - tire o
aria-busytambém quando a carga falhar (nofinally), senão a mensagem de erro fica presa dentro de uma área ocupada e o brilho não some; - coloque o texto de status, com
class="visually-hidden"erole="status", fora da área comaria-busy: NVDA e JAWS seguram os avisos de dentro de uma área ocupada até ela deixar de estar ocupada; - deixe a região de status sempre na página e mude só o texto dela ("Carregando contratos", depois "Contratos carregados"). Uma região que já entra na página com texto costuma não ser anunciada.
- marque a área com
- O esqueleto não recebe foco e não tem interação de teclado.
- A cor nunca é o único sinal: o texto de status diz que a área está carregando.
- O brilho respeita
prefers-reduced-motione, mesmo sem essa preferência, para sozinho antes de 5 s (WCAG 2.2.2). - Testado com axe nas três variantes em cada tamanho, sozinhas e dentro de uma área com
aria-busy, com o status fora dela e o texto dele definido depois da montagem. Um teste confere que a regra deprefers-reduced-motiondesliga a animação. - O jsdom não calcula layout, então as medidas foram conferidas no Chromium: 3 linhas
mddão 60 px, iguais a 3 linhas de texto de 14/20 px (sm: 54 px;lg: 72 px), e o círculomdfica com 40 × 40 px numa linha flex.