Skip to content
UI Kit
Accent color

Skeleton loader for React

Placeholders in the shape of the content, with a soft light that stays still for reduced motion.

One of 4 components in Progress and states

3 variants

Placeholders in the shape of the content

A skeleton shows the shape of what is coming while data loads, so the page does not jump when it arrives and people already know where to look. These skeletons copy the layout of the kit components, so the switch from placeholder to content keeps every line in place.

Cards fits grids of products, posts or people. List and dashboard covers rows with avatars and the blocks of an application screen, numbers, charts and tables. Article mirrors a post with title, meta line, picture and paragraphs. A soft light passes over the shapes, and with reduced motion it stays still.

Show a skeleton only for loads that take longer than about 300 milliseconds, shorter ones flicker. Match the number of placeholder rows to what usually arrives. Mark the region as busy for screen readers, the skeletons set aria-busy and hide their shapes from the reading order.

Variants

Cards

Article cards that swap to content without moving.

List and dashboard

An inbox list and a report with stats and a chart.

Article

A long read in its final rhythm while it loads.

Details

What Skeleton brings along, read from the registry and the source.

Variants
3
Version
1.0.0 · since 1.0.0
File
components/ui/skeleton.tsx
Brings along
utilskit
npm packages
None
Languages
English and German, 1 text of its own
Studio
Adjust in code

Props

The SkeletonProps interface from components/ui/skeleton.tsx, with the comments from the code.

  • shapeoptional
    'line' | 'circle' | 'rect'

    line: text height, rounded. circle: avatars. rect: images and cards.

  • styleoptional
    React.CSSProperties

Built-in texts

Skeleton writes these texts itself. The language follows the KitProvider, the labels prop replaces single ones.

Show all 1 texts
loadingEnglish LoadingGerman Wird geladen

Accessibility

Read from the source of the component.

  • Sets aria-busy and aria-live, so screen readers announce purpose and state.
  • Announces changes through a live region without moving focus.
  • Adds text for screen readers where icons and graphics stand alone.
  • The server renders the starting state in full, JavaScript only adds the interaction.

Motion

  • Brings animations of its own, calmly timed and without wobble.
  • With reduced motion (prefers-reduced-motion) it stays still.

Source

The file aiw add puts into your project. From then on it is yours.

Show the file, 112 lines
components/ui/skeleton.tsx
'use client';

import { useLayoutEffect, useRef, useState } from 'react';
import { cn, prefersReducedMotion, useTexts } from '@/lib/kit';

const TEXTS = {
  en: { loading: 'Loading' },
  de: { loading: 'Wird geladen' },
};

/*
 * A slow, slanted sheen that sweeps across and then rests, instead of a pulse.
 * All placeholders start together, so the light reads as one pass over the page.
 * When content arrives, the placeholders fade out over it and the content
 * comes in from a soft blur, one item after another.
 */
const STYLES =
  '.kitfx-sk::after{content:"";position:absolute;inset:0 -50%;transform:translateX(-60%) skewX(-18deg);background:linear-gradient(90deg,transparent 30%,color-mix(in oklab,var(--foreground) 5%,transparent) 44%,color-mix(in oklab,color-mix(in oklab,var(--primary) 30%,var(--foreground)) 9%,transparent) 50%,color-mix(in oklab,var(--foreground) 5%,transparent) 56%,transparent 70%);animation:kitfx-sk 2.4s cubic-bezier(.45,.05,.35,1) infinite}' +
  '@keyframes kitfx-sk{0%{transform:translateX(-60%) skewX(-18deg)}62%,100%{transform:translateX(60%) skewX(-18deg)}}' +
  '.kitfx-sk-ghost{animation:kitfx-sk-out .45s var(--ease-kit) forwards}@keyframes kitfx-sk-out{to{opacity:0;filter:blur(2px)}}' +
  '.kitfx-sk-reveal>:not(.kitfx-sk-ghost){animation:kitfx-sk-in .65s var(--ease-kit) both}' +
  '.kitfx-sk-reveal>:nth-child(2){animation-delay:70ms}.kitfx-sk-reveal>:nth-child(3){animation-delay:140ms}.kitfx-sk-reveal>:nth-child(4){animation-delay:210ms}.kitfx-sk-reveal>:nth-child(n+5){animation-delay:280ms}' +
  '@keyframes kitfx-sk-in{from{opacity:0;filter:blur(6px);transform:translateY(6px)}}' +
  '@media (prefers-reduced-motion:reduce){.kitfx-sk::after{animation:none;opacity:0}.kitfx-sk-ghost{display:none}.kitfx-sk-reveal>*{animation:none}}';

export interface SkeletonProps {
  /** line: text height, rounded. circle: avatars. rect: images and cards. */
  shape?: 'line' | 'circle' | 'rect';
  className?: string;
  style?: React.CSSProperties;
}

/**
 * A placeholder that holds the space of content still loading. A slanted light
 * sweeps over it and rests; with reduced motion it stays still. Decorative: wrap
 * a group in <SkeletonGroup> so screen readers hear "Loading" once.
 */
export function Skeleton({ shape = 'line', className, style }: SkeletonProps) {
  return (
    <>
      {/* React 19 hoists this once into the head, however many skeletons render */}
      <style href="kitfx-sk" precedence="default">
        {STYLES}
      </style>
      <span
        aria-hidden
        style={style}
        className={cn(
          'kitfx-sk relative block overflow-hidden bg-[color-mix(in_oklab,var(--foreground)_7%,transparent)]',
          shape === 'line' && 'h-3 rounded-full',
          shape === 'circle' && 'aspect-square rounded-full',
          shape === 'rect' && 'rounded-lg',
          className,
        )}
      />
    </>
  );
}

/** Several lines of text, the last one shorter, like a paragraph. */
export function SkeletonText({ lines = 3, className }: { lines?: number; className?: string }) {
  const widths = ['100%', '94%', '97%', '88%', '92%'];
  return (
    <span className={cn('block space-y-2.5', className)} aria-hidden>
      {Array.from({ length: lines }, (_, i) => (
        <Skeleton key={i} style={{ width: i === lines - 1 && lines > 1 ? '62%' : widths[i % widths.length] }} />
      ))}
    </span>
  );
}

/**
 * Wraps placeholders: marks the region busy and announces "Loading" once.
 * When `loading` turns false the children are replaced by `content`: the
 * placeholders fade out on top while the content comes in, so nothing jumps.
 */
export function SkeletonGroup({ loading = true, label, content, className, children }: { loading?: boolean; label?: string; content?: React.ReactNode; className?: string; children: React.ReactNode }) {
  const t = useTexts(TEXTS);
  const was = useRef(loading);
  const [swap, setSwap] = useState(false);
  // Before paint, so the content never shows a frame without its entrance
  useLayoutEffect(() => {
    const before = was.current;
    was.current = loading;
    if (!before || loading || prefersReducedMotion()) {
      setSwap(false);
      return;
    }
    setSwap(true);
    const timer = window.setTimeout(() => setSwap(false), 700);
    return () => window.clearTimeout(timer);
  }, [loading]);
  return (
    <div aria-busy={loading} aria-live="polite" className={cn(swap && 'kitfx-sk-reveal relative', className)}>
      {loading ? (
        <>
          <span className="sr-only">{label ?? t.loading}</span>
          {children}
        </>
      ) : (
        <>
          {content}
          {swap && (
            <div aria-hidden className={cn('kitfx-sk-ghost pointer-events-none absolute inset-0 z-10', className)} style={{ margin: 0 }}>
              {children}
            </div>
          )}
        </>
      )}
    </div>
  );
}

Frequent questions

How do I install Skeleton?

With aiw add @ai-website/skeleton. The command puts components/ui/skeleton.tsx into your project, the code is yours from then on. It needs no further npm packages.

Which variants does Skeleton have?

3 variants, Cards, List and dashboard and Article.

Can I translate the texts of Skeleton?

Yes. Skeleton carries 1 texts of its own in English and German, such as “Loading”. The language follows the KitProvider, single texts change through the labels prop.

Can an AI assistant build with Skeleton?

Yes. Through the MCP server of the aiw CLI, Claude, Cursor, GitHub Copilot, Windsurf, Codex and Gemini find Skeleton with aiw_components and install it with aiw_add @ai-website/skeleton.

Often searched

Jump to

Copied to the clipboard