HextaUI

Skeleton

Des placeholders qui attendent 150ms avant de s’afficher, prennent la taille exacte du contenu qu’ils enveloppent et l’affichent en fondu sans rien déplacer.

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

Ajoute le composant, les tokens de thème HextaUI et les composants HextaUI dont il dépend.

Le shimmer et la pulsation utilisent deux animations de votre thème. Ajoutez-les une fois à votre fichier 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;
    }
  }
}

Utilisez un skeleton seul comme placeholder dimensionné, ou passez loading et enveloppez le vrai contenu pour que le placeholder prenne sa taille exacte.

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

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

Formes

Seul, un skeleton est un bloc vide. Donnez-lui une taille et un rayon avec des classes pour correspondre à ce qu'il remplace.

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

Animations

animation choisit un shimmer qui balaie, une pulse douce, 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>
  )
}

Texte

<SkeletonText /> trace une barre par ligne et suit la taille de police et la hauteur de ligne du parent, pour occuper le même espace que le texte qu'il remplace. La dernière ligne est plus courte.

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

Envelopper du vrai contenu

Avec loading, le skeleton rend le vrai contenu de façon invisible en dessous, prend donc sa taille exacte, et rien ne bouge à l'arrivée des données. Quand loading passe à false, le contenu apparaît en fondu.

"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 ligne

Passez render={<span />} pour placer un skeleton dans une phrase. Il se pose sur la ligne de base du texte.

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

Chargement rapide

Un skeleton reste invisible pendant ses 150ms initiales, puis apparaît en fondu. Les données qui arrivent plus vite n'affichent jamais de placeholder en éclair.

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

Nombreuses lignes

Les skeletons montés ensemble démarrent leur animation ensemble, si bien qu'une longue liste se lit comme une seule zone de chargement.

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 droite à gauche

Dans une mise en page de droite à gauche, le shimmer balaie de droite à gauche.

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 seul est décoratif et masqué aux technologies d'assistance, tout comme <SkeletonText />.
  • Un skeleton qui enveloppe du contenu définit aria-busy pendant le chargement. Le contenu en dessous est masqué aux technologies d'assistance et ne peut pas recevoir le focus avant son chargement.
  • Les skeletons n'annoncent rien. Quand il faut savoir ce qui se charge, ajoutez un label visible ou un message de statut.
  • Avec la réduction des animations, les skeletons apparaissent aussitôt, sans shimmer, pulsation ni fondu.

Accepte tous les attributs de l'élément qu'il rend.

PropTypePar défaut
animation
"shimmer" | "pulse" | "none""shimmer"
loadingLorsqu'il est défini, le skeleton enveloppe ses enfants : true affiche le placeholder, false affiche le contenu. Omettez-le pour un placeholder autonome.
boolean–
render
ReactElement | (props, state) => ReactElement<div>
AttributDescription
data-slot="skeleton"Cible les skeletons en CSS.
data-animationL'animation utilisée. Retirée une fois le contenu enveloppé chargé.
data-loading"true" ou "false" quand le skeleton enveloppe du contenu.
data-slot="skeleton-content"Enveloppe le vrai contenu. Invisible et inerte pendant le chargement.
--skeleton-dir1, ou -1 dans les mises en page de droite à gauche. Définit la direction du shimmer.

Accepte tous les attributs de <div>.

PropTypePar défaut
linesArrondi à l'inférieur et borné entre 1 et 50.
number3
animation
"shimmer" | "pulse" | "none""shimmer"
AttributDescription
data-slot="skeleton-text"Le conteneur des lignes.

Utilisé dans les blocks

Des blocks qui s’appuient sur Skeleton.