# React Skeleton Loader: echte Formen, kein Layout Shift

> Bau einen React Skeleton Loader in der Form des echten Inhalts, nimm einen Spinner, wo ein Skeleton nicht passt, und tausch die Daten ohne Layout Shift ein.

- Autor: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Veröffentlicht: 19. Okt. 2026
- Kategorie: [Komponenten-Guides](https://wingo-ui.com/de/blog/category/components)
- Lesezeit: 9 Min.
- Kanonische URL: https://wingo-ui.com/de/blog/react-skeleton-loader
- Englische Fassung: https://wingo-ui.com/blog/react-skeleton-loader

## Kurz gesagt

Ein guter React Skeleton Loader hat dieselben Boxen, Zeilenhöhen und Ecken wie der Inhalt, für den er steht, damit die echten Daten an ihren Platz rücken, ohne dass sich etwas verschiebt. Nimm ihn für Inhalte mit bekanntem Layout wie Feeds, Listen, Karten und Tabellen, und nimm einen Spinner für Aktionen, einzelne kleine Module und Wartezeiten, bei denen das Layout unklar ist. Das kostenlose Skeleton von Wingo UI bringt Presets für Karten, Listen, Tabellen und Profile mit, dazu einen Wrapping-Modus, der die exakte Größe des echten Inhalts übernimmt.

Du lädst eine Liste, und eine Sekunde lang bleibt der Bildschirm leer. Also baust du einen React Skeleton Loader ein: graue Balken, die schimmern, bis die Daten da sind. Dann kommen die Daten, und die Seite springt, weil die Balken 16px hoch waren und der Text 20px, und weil aus dem quadratischen Platzhalter ein Foto im Format 16:10 wurde. Dieser Guide zeigt, wann ein Skeleton der richtige Ladezustand ist, wann ein Spinner besser passt, wie du Skeletons wie den echten Inhalt formst und wie du sie ohne Layout Shift austauschst, mit den kostenlosen Komponenten [Skeleton](https://wingo-ui.com/components/skeleton), [Spinner](https://wingo-ui.com/components/spinner) und [Empty State](https://wingo-ui.com/components/empty-state) von Wingo UI.

## Skeleton vs Spinner: Welchen Ladezustand soll ich nehmen?

Nimm ein Skeleton, wenn du das Layout dessen kennst, was kommt, und einen Spinner, wenn nicht. Die klarste Antwort auf die Frage Skeleton vs Spinner, die wir gefunden haben, gibt [Skeleton Screens 101](https://www.nngroup.com/articles/skeleton-screens/) der Nielsen Norman Group (Juni 2023, zuletzt geprüft im September 2026). Der Artikel beschränkt Skeletons auf das Laden ganzer Seiten; wir nutzen sie auch für seitengroße Bereiche wie einen Feed oder eine Tabelle, weil dieselbe Begründung gilt:

| Situation | Ladezustand | Warum |
|---|---|---|
| Unter etwa 1 Sekunde | Nichts | Jede Anzeige blitzt nur kurz auf |
| Eine ganze Seite oder ein seitengroßer Feed, eine Liste oder Tabelle, unter 10 Sekunden | Skeleton | Die Vorschau des Layouts zeigt, was kommt |
| Ein einzelnes Modul, etwa ein Video oder eine Dashboard-Karte | Spinner | Ein Skeleton aus einer Box sagt nichts, was ein Spinner nicht auch sagt |
| Eine Aktion: Speichern, Anmelden, Bezahlen | Spinner, meist im Button | Es gibt keinen Inhalt für eine Vorschau |
| Über 10 Sekunden, oder ein Upload oder Import | Fortschrittsbalken mit Schätzung | Eine Endlosanimation verschleiert, wie lange es dauert |

Laut demselben Artikel sollen die grauen Boxen die Struktur der fertigen Seite nachbilden, und Skeletons verkürzen die gefühlte Ladezeit. Gefühlt ist das entscheidende Wort: Die Anfrage dauert genauso lange. Wenn sie vier Sekunden braucht, optimier also auch die Anfrage.

## Wie baue ich einen React Skeleton Loader mit Tailwind?

Die minimale Antwort auf die Suche nach „react skeleton loader tailwind“ ist ein einzelnes div mit `animate-pulse`, einer Tönung, einem Radius und einer festen Größe. Stand Oktober 2026 ist das [Skeleton](https://ui.shadcn.com/docs/components/skeleton) von shadcn/ui genau das: ein div mit `animate-pulse rounded-md bg-accent` plus deinen Größenklassen. Das funktioniert, aber du musst jeden Block selbst an den echten Inhalt anpassen, die Wartezeit für Screenreader ansagen und den Austausch sauber hinbekommen. Das Skeleton von Wingo UI übernimmt diese Teile:

```bash
npx wingo-ui@latest add skeleton
```

Der Basisblock hat drei Formen. `rect` füllt die Zeile und ist 96px hoch, bis du ihm eine Größe gibst, `circle` ist 40px groß, solange `width` nichts anderes sagt, und `line` ist ein 14px hoher Textbalken. `width` und `height` nehmen px-Zahlen oder jede CSS-Länge, Tailwind-Klassen funktionieren auch.

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

<Skeleton shape="circle" width={40} />
<Skeleton shape="line" width="40%" />
<SkeletonText lines={2} size="sm" lastLineWidth="70%" />
```

`animation` kann `shimmer` (Standard), `pulse` oder `none` sein. Der Shimmer ist ein Streifen, der sich auf einem Pseudo-Element per `transform` bewegt statt über eine animierte `background-position`. So kann der Browser ihn auf dem Compositor laufen lassen, statt jeden Frame neu zu zeichnen. `speed` skaliert den Zyklus, `color` und `highlight` nehmen jede CSS-Farbe. Die Standardfüllung ist die Textfarbe mit 5 % Deckkraft und bleibt damit in Light und Dark Mode sichtbar.

Drück in der Demo unten auf Reload und schau dir die Inseratskarten an: Skeletons und echte Karten haben denselben Rhythmus, deshalb verschiebt der Austausch nichts.

> Live-Demo (Skeleton): Drück Reload, um eine 1,4 Sekunden lange Anfrage erneut abzuspielen: Die Skeleton-Karten werden zu Inseraten, ohne dass sich die Zeilenhöhe ändert. Stell shape auf circle oder line, um den Wrapping-Modus zu sehen. Probier sie unter [Skeleton](https://wingo-ui.com/components/skeleton) aus und installier sie mit `npx wingo-ui@latest add skeleton`.

## Wie forme ich ein Skeleton wie den echten Inhalt?

Auf zwei Wegen: Du umschließt den echten Inhalt, oder du nimmst ein Preset, das ein gängiges Layout nachbildet.

### Den echten Inhalt umschließen

Gib das echte Markup als children mit und dazu ein `loading`-Flag. Während des Ladens rendern die children unsichtbar und `inert` im Block, sodass der Block exakt ihre Größe annimmt. Ein `rect` übernimmt auch die Ecken dessen, was er umschließt, etwa eines abgerundeten Fotos oder eines Buttons. Sobald `loading` false wird, wird der Inhalt genau dort eingeblendet, wo der Block war.

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

export function SellerRow({ loading }: { loading: boolean }) {
  return (
    <div className="flex items-center gap-3">
      <Skeleton shape="circle" loading={loading}>
        <img src="/emma-carter.jpg" alt="Emma Carter" className="size-10 rounded-full" />
      </Skeleton>
      <Skeleton shape="line" loading={loading}>
        <p className="text-sm font-medium">Emma Carter</p>
      </Skeleton>
      <Skeleton loading={loading}>
        <Button variant="soft" size="sm">Folgen</Button>
      </Skeleton>
    </div>
  );
}
```

Das README von [react-loading-skeleton](https://github.com/dvtng/react-loading-skeleton) argumentiert genauso (Stand Oktober 2026): Bau Skeleton-Zustände direkt in deine Komponenten ein, statt separate Skeleton Screens zu pflegen, damit die Styles synchron bleiben. Dort haben die Balken standardmäßig die Höhe der Schriftgröße, und die Grundnutzung importiert eine CSS-Datei. Der Wrapping-Modus hier misst stattdessen das echte Element, sodass auch ein Button oder ein Foto seine echte Box bekommt.

Zum Umschließen brauchst du Inhalt, den man messen kann. Der Modus eignet sich also für Labels, Avatare, Buttons und Text, die du schon hast. Für Elemente, die du noch nicht hast, nimmst du ein Preset.

### Ein Preset nehmen

Vier Presets decken die Layouts ab, die am häufigsten geladen werden:

- `SkeletonCard`: ein Medienblock im Format 16:10 (`mediaAspect` ändert ihn), ein Titel und Meta-Zeilen sowie ein Footer mit Preiszeile und runder Aktion.
- `SkeletonList`: 56px hohe Zeilen mit 40px-Avatar, Titel, Untertitel und einer Aktion in Button-Größe, mit Haarlinien zwischen den Zeilen.
- `SkeletonTable`: ein Header und 48px hohe Zeilen ab 768px, darunter eine Karte pro Zeile, in reinem CSS, sodass das Server-Markup auf dem Smartphone schon stimmt. Das entspricht dem [responsiven Tabellen-Pattern](https://wingo-ui.com/de/blog/responsive-table-mobile-cards), bei dem Zeilen zu Karten werden.
- `SkeletonProfile`: ein Cover, ein Avatar, der es überlappt, Name und Meta-Zeilen und eine Reihe Kennzahlen.

Die Zeilenbreiten in den Presets stammen aus festen Listen, nicht aus `Math.random()`. Zufällige Breiten sehen natürlich aus, aber Server und Client würfeln unterschiedliche Zahlen, und React meldet einen Hydration Mismatch.

## Wie vermeide ich Layout Shift, wenn der Inhalt lädt?

Gib Skeleton und Inhalt dieselbe Größe, und lass den reservierten Platz nie zusammenfallen. Googles [Guide zur Optimierung von CLS](https://web.dev/articles/optimize-cls) setzt einen guten Cumulative Layout Shift bei 0,1 oder weniger an, empfiehlt, Platz für spät geladene Inhalte mit `min-height` oder `aspect-ratio` zu reservieren, und warnt, dass das Entfernen reservierten Platzes die Seite genauso verschiebt wie das Einfügen von Inhalt.

Daraus folgen fünf Regeln:

1. **Gleiche Zeilenhöhen.** `SkeletonCard` nutzt eine 20px hohe Titelzeile, 18px hohe Meta-Zeilen und einen 32px hohen Footer, der Balken jeweils mittig in der Zeile. Gib deiner echten Karte `leading-5`, `leading-[18px]` und `h-8` auf denselben Zeilen.
2. **Gleiche Medienbox.** Das `aspect-[16/10]` des Skeletons muss zum `aspect-[16/10]` des Bildes passen, nicht zu einer festen Höhe, die nur bei einer Breite stimmt.
3. **Gleiches Grid.** Render die Skeletons in denselben Grid-Klassen wie die echten Elemente.
4. **Ungefähr gleiche Anzahl.** Zeig etwa so viele Skeletons wie ein Bildschirm voller echter Elemente. Sechs Platzhalter, aus denen zwei Treffer werden, sind auch ein Shift.
5. **Behalt die Box, wenn nichts kommt.** Wechsle im selben Container zu einem Empty State, statt gar nichts zu rendern.

Hier das ganze Pattern in einer Seite mit dem Next.js App Router. `<Suspense>` streamt die statische Überschrift sofort und die Inserate, sobald sie bereit sind, so wie es die [Next.js-Doku zum Data Fetching](https://nextjs.org/docs/app/getting-started/fetching-data) beschreibt. Eine Datei `loading.tsx` neben der Seite macht dasselbe für das ganze Routensegment.

```tsx
// app/listings/page.tsx
import { Suspense } from "react";
import { SkeletonCard } from "@/components/ui/skeleton";
import { EmptyState } from "@/components/ui/empty-state";
import { getListings } from "@/lib/listings";

const GRID = "mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-3";

export default function ListingsPage() {
  return (
    <main className="mx-auto max-w-5xl px-4 py-8">
      <h1 className="text-2xl font-semibold tracking-tight">Wohnungen in New York</h1>
      <Suspense fallback={<ListingsSkeleton />}>
        <Listings />
      </Suspense>
    </main>
  );
}

function ListingsSkeleton() {
  return (
    <div className={GRID}>
      {Array.from({ length: 6 }, (_, i) => (
        // eine ansage für das ganze grid, nicht sechs
        <SkeletonCard key={i} label="Inserate werden geladen" aria-hidden={i > 0 || undefined} />
      ))}
    </div>
  );
}

async function Listings() {
  const listings = await getListings();
  if (listings.length === 0) {
    return (
      <EmptyState
        preset="no-results"
        variant="surface"
        className="mt-6 min-h-80"
        title="Noch keine passenden Wohnungen"
        description="Vergrößere das Suchgebiet oder entferne einen Filter."
      />
    );
  }
  return (
    <div className={GRID}>
      {listings.map((listing) => (
        <article key={listing.id} className="flex flex-col gap-3">
          <img src={listing.photo} alt="" className="aspect-[16/10] w-full rounded-[10px] object-cover" />
          <div className="flex flex-col gap-1">
            <h2 className="truncate text-[15px] font-semibold leading-5">{listing.title}</h2>
            <p className="truncate text-[13px] leading-[18px] text-muted-foreground">{listing.area}</p>
          </div>
          <div className="flex h-8 items-center">
            <span className="text-lg font-semibold tabular-nums">{listing.price}</span>
          </div>
        </article>
      ))}
    </div>
  );
}
```

Skeleton und Empty State sind Client Components; eine Server Component kann sie rendern, weil die Props hier reine Daten sind. Die Bildecken sind `rounded-[10px]`, also der Standard `radius="md"` des Presets.

## Wie mache ich Skeleton Loading in React barrierefrei?

Sag die Wartezeit einmal an und halt die grauen Blöcke stumm. Jeder Block ist `aria-hidden`, und jedes Preset ist eine `role="status"`-Region mit `aria-busy` und einem versteckten Label, standardmäßig „Loading“. Setz `label` auf etwas Konkretes („Rechnungen werden geladen“). Zusätzliche Props landen nach den eigenen Attributen auf dem Root des Presets. Deshalb macht `aria-hidden` auf den weiteren Karten im Grid oben diese stumm.

Umschlossener Inhalt ist `inert`, solange er lädt, sodass Tab einen Button überspringt, der noch nicht bereit ist. Unter `prefers-reduced-motion` wird der Shimmer ausgeblendet und der Pulse stoppt, sodass jeder Block stillsteht. Die NN/g weist darauf hin, dass animierte Skeletons ablenken können, deshalb ist das wichtig. Der [Guide zu prefers-reduced-motion](https://wingo-ui.com/de/blog/prefers-reduced-motion-react) zeigt den Hook, der diesen Schalter hydration-sicher hält.

## Wann ist ein Spinner die bessere React Ladeanimation?

Wenn es kein Layout gibt, das man vorab zeigen könnte. Ein Spinner ist die richtige React Ladeanimation für ein abgeschicktes Formular, eine Anmeldung, eine Zahlung, Pull to Refresh oder ein einzelnes Panel, dessen Inhalt seine Form ändert. Der Spinner von Wingo UI hat sechs Stile (`ring`, `dots`, `bars`, `pulse`, `orbit` und das iOS-ähnliche `spokes`), die Größen `sm`, `md` und `lg` mit 16, 24 und 40px und ein `label`, das Screenreader einmal als Statusmeldung vorlesen, ohne andere Ansagen zu unterbrechen.

Seine nützlichste Prop ist `delay`. Mit `delay={300}` rendert in den ersten 300 Millisekunden nichts. Eine schnelle Antwort lässt also nie einen Spinner aufblitzen, und bei einer langsamen wird er eingeblendet.

```tsx
import { Spinner } from "@/components/ui/spinner";

export function SearchPending() {
  return (
    <div className="flex min-h-52 flex-col items-center justify-center gap-3 text-muted-foreground">
      <Spinner variant="spokes" size="lg" delay={300} label="Wohnungen werden gesucht" />
      <p className="text-sm">Suche Wohnungen in New York…</p>
    </div>
  );
}
```

Das `min-h-52` reserviert Platz, wie es ein Skeleton tun würde. Für Aktionen lässt du den separaten Spinner weg und nimmst die Prop `loading` der [Button-Komponente](https://wingo-ui.com/components/button). Der Button behält seine Breite, während er einen Spinner zeigt, und `loadingText="Speichern"` setzt ein Label daneben.

> Live-Demo (Spinner): Die Verzögerung steht auf 300ms. Drück Reload: Der Spinner bleibt erst versteckt und blendet dann ein, sobald die Wartezeit lang genug ist, um sie zu bemerken. Probier sie unter [Spinner](https://wingo-ui.com/components/spinner) aus und installier sie mit `npx wingo-ui@latest add spinner`.

## Was ersetzt ein Skeleton, wenn die Anfrage fehlschlägt oder nichts zurückgibt?

Ein Empty State in derselben Box. Der Empty State hat Presets für `empty`, `no-results`, `error`, `first-run`, `no-permission` und `offline`, jedes mit eigenem Icon, eigenem Farbton und eigenem Text. Beim Preset `error` oder `offline` ergänzt `onRetry` einen Button „Try again“, der einen Spinner zeigt, bis das Promise, das du zurückgibst, erledigt ist. Weil `onRetry` eine Funktion ist, renderst du diesen Fall aus einer Client Component.

## Was sind die typischen Fehler beim Skeleton Loading?

Beim Skeleton Loading React-seitig zeigen sich drei Fehler erst, wenn echte Daten und echte Netzwerke ins Spiel kommen.

- **Ein Farbton für alles.** Ein Skeleton auf einer Karte im selben Grau verschwindet. Die Standardtönung bezieht sich auf die Textfarbe und ist deshalb auf jeder Fläche sichtbar; auf einem Banner in Markenfarbe übergibst du Weiß an `color` und `highlight`.
- **Skeletons für eine Anfrage von 200ms.** Wenn die meisten Antworten schnell kommen, blitzt das Skeleton nur auf. Zeig gecachte Daten, oder überbrück die Wartezeit mit einem Spinner mit `delay`.
- **Ein Skeleton, das nie endet.** Eine fehlgeschlagene Anfrage muss das Skeleton durch eine Fehlermeldung mit Retry ersetzen, sonst warten die Leute ewig.

## Wie füge ich diese Komponenten meinem Projekt hinzu?

Führ `npx wingo-ui@latest add skeleton spinner empty-state` aus. Die CLI kopiert den Quellcode in dein Projekt, samt der Helfer und Komponenten, die er importiert (der Empty State bringt den Button mit), und installiert die npm-Pakete, die er nutzt (`motion`, `lucide-react` und `@radix-ui/react-slot`). Alle drei sind kostenlos. Wenn jedes Skeleton in deiner App pulsieren statt schimmern soll, stell das einmal in `<UIProvider defaults>` ein, statt die Prop jedes Mal mitzugeben. Der Basisblock liest den Schlüssel `skeleton`; `SkeletonText` und jedes Preset lesen jeweils ihren eigenen (`skeleton-text`, `skeleton-card`, `skeleton-list`, `skeleton-table`, `skeleton-profile`), weil sie `animation` explizit an ihre Blöcke weitergeben. `defaults={{ skeleton: { animation: "pulse" }, "skeleton-card": { animation: "pulse" } }}` deckt also einfache Blöcke und Karten ab. Mehr Deep Dives zu einzelnen Komponenten findest du in der [Kategorie Komponenten-Guides](https://wingo-ui.com/de/blog/category/components), und der [Guide zu React-Formularkomponenten](https://wingo-ui.com/de/blog/react-form-components-guide) wendet dieselben Regeln für Barrierefreiheit und Mobile auf jedes Formularelement an.

## Komponenten in diesem Artikel

- [Skeleton](https://wingo-ui.com/components/skeleton) (Free): A React skeleton loader shaped like the content on its way: a shimmering block, text lines and presets for cards, lists and tables. Installation: `npx wingo-ui@latest add skeleton`
- [Spinner](https://wingo-ui.com/components/spinner) (Free): A React loading spinner for waits without a known shape: six styles, from a quiet ring to the iOS spokes, in any size, tone or speed. Installation: `npx wingo-ui@latest add spinner`
- [Empty State](https://wingo-ui.com/components/empty-state) (Free): A React empty state for lists, tables, searches and pages with nothing to show yet: an icon tile, a title, a description and actions. Installation: `npx wingo-ui@latest add empty-state`

## Häufige Fragen

### Soll ich ein Skeleton oder einen Spinner verwenden?

Nimm ein Skeleton, wenn du das Layout des ladenden Inhalts kennst, etwa bei einem Feed, einer Liste oder einer ganzen Seite. Nimm einen Spinner für Aktionen wie Speichern oder Anmelden, für ein einzelnes kleines Modul und wenn du nicht vorhersagen kannst, was erscheint. Ab etwa 10 Sekunden zeigst du besser einen Fortschrittsbalken.

### Wie baue ich einen Skeleton Loader mit Tailwind CSS?

Die einfachste Version ist ein div mit animate-pulse, einer Tönung wie bg-foreground/5, einem Radius und fester Höhe und Breite. Gib jedem Block die Höhe der Textzeile oder des Bildes, das er ersetzt, sonst springt die Seite, wenn die Daten kommen.

### Verursachen Skeleton Screens Layout Shift?

Nur wenn sie eine andere Größe haben als der Inhalt, der sie ersetzt. Übernimm Zeilenhöhen, Abstände, Seitenverhältnisse der Bilder und das Grid, und behalt den reservierten Platz auch dann, wenn die Anfrage nichts zurückgibt.

### Sind Skeleton Loader barrierefrei?

Sie können es sein. Versteck die grauen Blöcke vor Screenreadern, lass ein einziges kurzes Label wie „Inserate werden geladen“ ansagen, halt den Fokus aus Inhalten heraus, die noch nicht bereit sind, und stopp den Shimmer, wenn jemand reduzierte Bewegung eingestellt hat.

### Ist das Skeleton von Wingo UI kostenlos?

Ja. Skeleton, Spinner und Empty State sind alle kostenlose Komponenten. Du installierst sie mit npx wingo-ui@latest add skeleton spinner empty-state.

---

Quelle: https://wingo-ui.com/de/blog/react-skeleton-loader
