HextaUI

Skeleton

Placeholders que esperam 150ms antes de aparecer, assumem o tamanho exato do conteúdo que envolvem e o fazem surgir com fade sem mover nada.

import { Skeleton } from "@/components/ui/skeleton"

export function SkeletonDemo() {
  return (
    <div className="flex w-full max-w-sm items-center gap-4">
      <Skeleton className="size-12 rounded-full" />
      <div className="flex flex-1 flex-col gap-2">
        <Skeleton className="h-4 w-3/5" />
        <Skeleton className="h-4 w-4/5" />
      </div>
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/skeleton.json

Adiciona o componente, os tokens de tema do HextaUI e quaisquer componentes do HextaUI dos quais ele depende.

O shimmer e o pulse usam duas animações do seu tema. Adicione-as uma vez ao seu arquivo CSS global.

@import "tw-animate-css";

@theme {
  --animate-shimmer: shimmer 1.6s ease-in-out infinite;
  --animate-skeleton-pulse: skeleton-pulse 2s ease-in-out infinite;

  @keyframes shimmer {
    from {
      translate: calc(-100% * var(--skeleton-dir, 1)) 0;
    }
    to {
      translate: calc(100% * var(--skeleton-dir, 1)) 0;
    }
  }

  @keyframes skeleton-pulse {
    0%,
    100% {
      opacity: 0;
    }
    50% {
      opacity: 0.5;
    }
  }
}

Use um skeleton sozinho como placeholder dimensionado, ou passe loading e envolva o conteúdo real para que o placeholder assuma seu tamanho exato.

import { Skeleton, SkeletonText } from "@/components/ui/skeleton"
<Skeleton className="h-4 w-32" />

<Skeleton loading={isLoading}>
  <h3>{user.name}</h3>
</Skeleton>

Formas

Sozinho, um skeleton é um bloco vazio. Dê a ele tamanho e raio com classes para combinar com o que ele representa.

import { Skeleton } from "@/components/ui/skeleton"

export function SkeletonShapes() {
  return (
    <div className="flex w-full max-w-md flex-col gap-4">
      <div className="flex items-center gap-4">
        <Skeleton className="size-12 rounded-full" />
        <div className="flex flex-1 flex-col gap-2">
          <Skeleton className="h-4 w-2/5" />
          <Skeleton className="h-4 w-3/5" />
        </div>
        <Skeleton className="h-8 w-20" />
      </div>
      <Skeleton className="aspect-video w-full rounded-xl" />
    </div>
  )
}

Animações

animation escolhe um shimmer de varredura, um pulse suave ou none.

import { Skeleton, type SkeletonAnimation } from "@/components/ui/skeleton"

const animations: SkeletonAnimation[] = ["shimmer", "pulse", "none"]

export function SkeletonAnimations() {
  return (
    <div className="grid w-full max-w-md grid-cols-3 gap-3">
      {animations.map((animation) => (
        <div key={animation} className="flex flex-col gap-2">
          <Skeleton animation={animation} className="h-16 w-full" />
          <span className="text-xs text-muted-foreground">{animation}</span>
        </div>
      ))}
    </div>
  )
}

Texto

<SkeletonText /> desenha uma barra por linha e segue o tamanho e a altura de linha da fonte do pai, então ocupa o mesmo espaço do texto que substitui. A última linha é mais curta.

import { SkeletonText } from "@/components/ui/skeleton"

export function SkeletonTextDemo() {
  return (
    <div className="grid w-full max-w-md grid-cols-3 gap-6">
      <div className="text-sm">
        <SkeletonText lines={3} />
      </div>
      <div className="text-lg">
        <SkeletonText lines={3} />
      </div>
      <div className="text-sm leading-8">
        <SkeletonText lines={3} />
      </div>
    </div>
  )
}

Envolver conteúdo real

Com loading, o skeleton renderiza o conteúdo real invisível por baixo, assumindo o tamanho exato, e nada se desloca quando os dados chegam. Quando loading vira false, o conteúdo aparece com fade.

"use client"

import * as React from "react"
import { IconUserPlus } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import { Skeleton } from "@/components/ui/skeleton"

export function SkeletonWrapContent() {
  const [loading, setLoading] = React.useState(true)

  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      <div className="flex items-start gap-4 rounded-xl border p-4">
        <Skeleton loading={loading} className="rounded-full">
          <img
            src="/preview/landscape.svg"
            alt=""
            className="size-12 rounded-full object-cover"
          />
        </Skeleton>
        <div className="flex min-w-0 flex-1 flex-col gap-1">
          <Skeleton loading={loading}>
            <h3 className="font-semibold">Olivia Martin</h3>
          </Skeleton>
          <Skeleton loading={loading}>
            <p className="text-sm text-muted-foreground">
              Design engineer at Acme. Writes about motion and small details.
            </p>
          </Skeleton>
        </div>
        <Skeleton loading={loading}>
          <Button size="sm" variant="outline">
            <IconUserPlus data-icon="inline-start" />
            Follow
          </Button>
        </Skeleton>
      </div>
      <div className="flex gap-2">
        <Button size="sm" onClick={() => setLoading(false)}>
          Load
        </Button>
        <Button size="sm" variant="ghost" onClick={() => setLoading(true)}>
          Reset
        </Button>
      </div>
    </div>
  )
}

Inline

Passe render={<span />} para colocar um skeleton dentro de uma frase. Ele fica na linha de base do texto.

import { Skeleton } from "@/components/ui/skeleton"

export function SkeletonInline() {
  return (
    <p className="text-sm">
      Your balance is{" "}
      <Skeleton loading render={<span />}>
        <strong>$12,480.00</strong>
      </Skeleton>{" "}
      as of today.
    </p>
  )
}

Carregamento rápido

Um skeleton fica invisível nos primeiros 150ms e depois aparece com fade. Dados que chegam antes nunca fazem um placeholder piscar.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { Skeleton } from "@/components/ui/skeleton"

export function SkeletonFastLoad() {
  const [loading, setLoading] = React.useState(true)
  const timer = React.useRef<ReturnType<typeof setTimeout>>(undefined)

  React.useEffect(() => () => clearTimeout(timer.current), [])

  function run() {
    clearTimeout(timer.current)
    setLoading(true)
    timer.current = setTimeout(() => setLoading(false), 80)
  }

  return (
    <div className="flex flex-col items-center gap-3">
      <Skeleton loading={loading}>
        <p className="text-sm">
          Loaded in 80ms, so the skeleton never became visible.
        </p>
      </Skeleton>
      <Button size="sm" onClick={run}>
        Run fast load
      </Button>
    </div>
  )
}

Muitas linhas

Skeletons montados juntos iniciam a animação juntos, então uma lista longa se lê como uma única área de carregamento.

import { Skeleton } from "@/components/ui/skeleton"

export function SkeletonManyRows() {
  return (
    <div className="flex max-h-64 w-full max-w-sm flex-col gap-3 overflow-y-auto">
      {Array.from({ length: 50 }, (_, index) => (
        <div key={index} className="flex items-center gap-3">
          <Skeleton className="size-8 rounded-full" />
          <Skeleton className="h-3 w-1/2" />
        </div>
      ))}
    </div>
  )
}

Da direita para a esquerda

Em um layout da direita para a esquerda, o shimmer varre da direita para a esquerda.

import { Skeleton, SkeletonText } from "@/components/ui/skeleton"

export function SkeletonRtl() {
  return (
    <div dir="rtl" className="flex w-full max-w-sm items-center gap-4 text-sm">
      <Skeleton className="size-12 rounded-full" />
      <div className="flex-1">
        <SkeletonText lines={2} />
      </div>
    </div>
  )
}
  • Um skeleton sozinho é decorativo e fica oculto das tecnologias assistivas, assim como <SkeletonText />.
  • Um skeleton que envolve conteúdo define aria-busy enquanto carrega. O conteúdo por baixo fica oculto das tecnologias assistivas e não pode receber foco até carregar.
  • Skeletons não anunciam nada. Quando as pessoas precisam saber o que está carregando, adicione um rótulo visível ou uma mensagem de status.
  • Com movimento reduzido ativado, os skeletons aparecem de imediato, sem shimmer, pulse ou fade.

Aceita todos os atributos do elemento que renderiza.

PropTipoPadrão
animation
"shimmer" | "pulse" | "none""shimmer"
loadingQuando definido, o skeleton envolve seus filhos: true mostra o placeholder, false mostra o conteúdo. Omita para um placeholder independente.
boolean–
render
ReactElement | (props, state) => ReactElement<div>
AtributoDescrição
data-slot="skeleton"Seleciona os skeletons no CSS.
data-animationA animação em uso. Removida quando o conteúdo envolvido terminou de carregar.
data-loading"true" ou "false" quando o skeleton envolve conteúdo.
data-slot="skeleton-content"Envolve o conteúdo real. Invisível e inerte durante o carregamento.
--skeleton-dir1, ou -1 em layouts da direita para a esquerda. Define a direção do shimmer.

Aceita todos os atributos de <div>.

PropTipoPadrão
linesArredondado para baixo e mantido entre 1 e 50.
number3
animation
"shimmer" | "pulse" | "none""shimmer"
AtributoDescrição
data-slot="skeleton-text"O contêiner das linhas.

Usado em blocos

Blocos que se baseiam em Skeleton.