HextaUI

Skeleton

Marcadores de posición que esperan 150ms antes de mostrarse, toman el tamaño exacto del contenido que envuelven y lo hacen aparecer con un fundido sin 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

Añade el componente, los tokens del tema de HextaUI y los componentes de HextaUI de los que depende.

El shimmer y el pulso usan dos animaciones de tu tema. Añádelas una vez a tu archivo 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;
    }
  }
}

Usa un skeleton por sí solo como marcador de posición con tamaño, o pasa loading y envuelve el contenido real para que el marcador adopte su tamaño exacto.

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

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

Formas

Por sí solo, un skeleton es un bloque vacío. Dale tamaño y radio con clases para que coincida con lo que sustituye.

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>
  )
}

Animaciones

animation elige un shimmer de barrido, un pulse suave o 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 /> dibuja una barra por línea y sigue el tamaño de fuente y la altura de línea del padre, así que ocupa el mismo espacio que el texto al que sustituye. La última línea es más corta.

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 contenido real

Con loading, el skeleton renderiza el contenido real de forma invisible debajo, así que adopta el tamaño exacto y nada se desplaza cuando llegan los datos. Cuando loading pasa a false, el contenido aparece con un fundido.

"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>
  )
}

En línea

Pasa render={<span />} para colocar un skeleton dentro de una frase. Se sitúa sobre la línea base del 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>
  )
}

Carga rápida

Un skeleton permanece invisible durante sus primeros 150ms y luego aparece con un fundido. Los datos que llegan antes nunca muestran un marcador de posición parpadeante.

"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>
  )
}

Muchas filas

Los skeletons que se montan juntos inician su animación juntos, así que una lista larga se lee como una sola área de carga.

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>
  )
}

De derecha a izquierda

En un diseño de derecha a izquierda el shimmer barre de derecha a izquierda.

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>
  )
}
  • Un skeleton por sí solo es decorativo y se oculta a las tecnologías de asistencia, igual que <SkeletonText />.
  • Un skeleton que envuelve contenido define aria-busy mientras carga. El contenido de debajo se oculta a las tecnologías de asistencia y no se puede enfocar hasta que carga.
  • Los skeletons no anuncian nada. Cuando la gente necesite saber qué se está cargando, añade una etiqueta visible o un mensaje de estado.
  • Con movimiento reducido activado, los skeletons aparecen de inmediato sin shimmer, pulso ni fundido.

Acepta todos los atributos del elemento que renderiza.

PropTipoPredeterminado
animation
"shimmer" | "pulse" | "none""shimmer"
loadingCuando se define, el skeleton envuelve a sus hijos: true muestra el marcador y false muestra el contenido. Omítelo para un marcador independiente.
boolean–
render
ReactElement | (props, state) => ReactElement<div>
AtributoDescripción
data-slot="skeleton"Apunta a los skeletons en CSS.
data-animationLa animación en uso. Se elimina cuando el contenido envuelto ha cargado.
data-loading"true" o "false" cuando el skeleton envuelve contenido.
data-slot="skeleton-content"Envuelve el contenido real. Invisible e inerte mientras carga.
--skeleton-dir1, o -1 en diseños de derecha a izquierda. Define la dirección del shimmer.

Acepta todos los atributos de <div>.

PropTipoPredeterminado
linesSe redondea hacia abajo y se mantiene entre 1 y 50.
number3
animation
"shimmer" | "pulse" | "none""shimmer"
AtributoDescripción
data-slot="skeleton-text"El contenedor de las líneas.

Usado en bloques

Bloques que se construyen sobre Skeleton.