HextaUI

Skeleton

Platzhalter, die 150 ms warten, bevor sie erscheinen, exakt die Größe des umschlossenen Inhalts annehmen und ihn einblenden, ohne etwas zu verschieben.

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

Fügt die Komponente, die HextaUI-Theme-Tokens und alle HextaUI-Komponenten hinzu, von denen sie abhängt.

Shimmer und Pulse nutzen zwei Animationen aus deinem Theme. Füge sie einmal zu deiner globalen CSS-Datei hinzu.

@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;
    }
  }
}

Nutze ein Skeleton allein als dimensionierten Platzhalter oder übergib loading und umschließe den echten Inhalt, damit der Platzhalter dessen exakte Größe annimmt.

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

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

Formen

Für sich ist ein Skeleton ein leerer Block. Gib ihm per Klassen Größe und Radius passend zu dem, wofür es steht.

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

Animationen

animation wählt einen streichenden shimmer, einen weichen pulse oder 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>
  )
}

Text

<SkeletonText /> zeichnet einen Balken pro Zeile und folgt Schriftgröße und Zeilenhöhe des Elternelements, sodass es denselben Platz füllt wie der Text, den es ersetzt. Die letzte Zeile ist kürzer.

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

Echten Inhalt umschließen

Mit loading rendert das Skeleton den echten Inhalt unsichtbar darunter, sodass es die exakte Größe annimmt und sich nichts verschiebt, wenn die Daten eintreffen. Wird loading false, blendet der Inhalt ein.

"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

Übergib render={<span />}, um ein Skeleton in einen Satz zu setzen. Es sitzt auf der Textgrundlinie.

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

Schnelles Laden

Ein Skeleton bleibt in den ersten 150ms unsichtbar und blendet dann ein. Daten, die früher eintreffen, lassen nie einen Platzhalter aufblitzen.

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

Viele Zeilen

Skeletons, die zusammen gemountet werden, starten ihre Animation zusammen, sodass eine lange Liste wie ein Ladebereich wirkt.

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

Rechts nach links

In einem rechts-nach-links-Layout streicht der Shimmer von rechts nach links.

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>
  )
}
  • Ein einzelnes Skeleton ist Dekoration und vor assistiver Technik verborgen, ebenso <SkeletonText />.
  • Ein Skeleton, das Inhalt umschließt, setzt beim Laden aria-busy. Der Inhalt darunter ist vor assistiver Technik verborgen und nicht fokussierbar, bis er geladen ist.
  • Skeletons sagen nichts an. Wenn Leute wissen müssen, was lädt, füge ein sichtbares Label oder eine Statusmeldung hinzu.
  • Bei aktivierter reduzierter Bewegung erscheinen Skeletons sofort ohne Shimmer, Pulse oder Fade.

Akzeptiert alle Attribute des Elements, das es rendert.

PropTypStandard
animation
"shimmer" | "pulse" | "none""shimmer"
loadingWenn gesetzt, umschließt das Skeleton seine Kinder: true zeigt den Platzhalter, false den Inhalt. Lass es für einen eigenständigen Platzhalter weg.
boolean–
render
ReactElement | (props, state) => ReactElement<div>
AttributBeschreibung
data-slot="skeleton"Skeletons per CSS ansprechen.
data-animationDie verwendete Animation. Entfernt, sobald umschlossener Inhalt geladen ist.
data-loading"true" oder "false", wenn das Skeleton Inhalt umschließt.
data-slot="skeleton-content"Umschließt den echten Inhalt. Unsichtbar und inert, solange geladen wird.
--skeleton-dir1 oder -1 in rechts-nach-links-Layouts. Legt die Richtung des Shimmers fest.

Akzeptiert alle <div>-Attribute.

PropTypStandard
linesAbgerundet und zwischen 1 und 50 gehalten.
number3
animation
"shimmer" | "pulse" | "none""shimmer"
AttributBeschreibung
data-slot="skeleton-text"Der Container für die Zeilen.

In Blocks verwendet

Blocks, die auf Skeleton aufbauen.