# Skeleton

> Loading placeholder with shimmer. Rect by default, circle for avatars, text for stacked line bars. Renders `<div role="status" aria-busy="true">`.

- Category: data-display
- Status: stable (since 1.0.0)
- A11y pattern: https://www.w3.org/WAI/ARIA/apg/patterns/
- Tokens: --background-tertiary, --background-quaternary
- Playground: https://design.freecodecamp.org/playground#skeleton
- npm dependencies: `react@>=18 <20`
- Registry dependencies: [theme](https://design.freecodecamp.org/registry/theme.md)
- Files:
  - `Skeleton.tsx` → `src/ui/skeleton/Skeleton.tsx` (raw: https://design.freecodecamp.org/registry/skeleton/Skeleton.tsx)
  - `skeleton.css` → `src/ui/skeleton/skeleton.css` (raw: https://design.freecodecamp.org/registry/skeleton/skeleton.css)

## Install (copy source)

1. Ensure the theme is installed once per project - tokens.css + base.css imported globally, fonts available. See https://design.freecodecamp.org/registry/theme.md and https://design.freecodecamp.org/registry/starter.md.
2. Copy the files below into `src/ui/skeleton/` (adjust to your project layout) and import the CSS once from your global stylesheet, e.g. `@import './ui/skeleton/skeleton.css';`.
3. Colors, spacing and type come from tokens - tailor the component by editing the copied source; recolour by editing tokens.css, not the component CSS.

## Usage

Use Skeleton to reserve layout space while real content loads. The
shimmer animation hints "something is coming" without yanking focus
the way a spinner does. Pass `label` for an `aria-live` announcement
hidden behind `.sr-only`.

## Accessibility

Skeleton emits `role="status"` + `aria-busy="true"` + `aria-live="polite"`
so assistive tech hears a loading cue. Keep the `label` terse - announce
what is loading, not how long it will take. Animation pauses under
`prefers-reduced-motion: reduce`.

## Example

```tsx
import { Skeleton } from './ui/skeleton/Skeleton';

<Skeleton variant="circle" size={48} />
<Skeleton variant="text" width="80%" />
<Skeleton variant="text" width="60%" />
```

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `variant` | `enum` | no | `rect` |  |
| `width` | `string | number` | no | - |  |
| `height` | `string | number` | no | - |  |
| `lines` | `number` | no | - | For variant="text": render N stacked line bars. |
| `label` | `ReactNode` | no | - | Screen-reader label announced via visually-hidden span. |

## Source: Skeleton.tsx

```tsx
import React, { forwardRef } from 'react';

export type SkeletonVariant = 'rect' | 'circle' | 'text';

export interface SkeletonProps extends Omit<
  React.HTMLAttributes<HTMLDivElement>,
  'children'
> {
  variant?: SkeletonVariant;
  width?: number | string;
  height?: number | string;
  /** For variant="text": render N stacked line bars. */
  lines?: number;
  /** Screen-reader label announced via visually-hidden span. */
  label?: React.ReactNode;
}

const toSize = (v: number | string | undefined): string | undefined =>
  typeof v === 'number' ? `${v}px` : v;

export const Skeleton = forwardRef<HTMLDivElement, SkeletonProps>(
  (
    {
      variant = 'rect',
      width,
      height,
      lines,
      label,
      className = '',
      style,
      ...rest
    },
    ref
  ) => {
    const classes = [
      'skeleton',
      variant !== 'rect' && `skeleton--${variant}`,
      className
    ]
      .filter(Boolean)
      .join(' ');
    const mergedStyle: React.CSSProperties = {
      ...(style ?? {}),
      ...(width !== undefined && { width: toSize(width) }),
      ...(height !== undefined && { height: toSize(height) })
    };
    const hasInlineStyle = Object.keys(mergedStyle).length > 0;
    const isMultilineText =
      variant === 'text' && typeof lines === 'number' && lines > 0;
    return (
      <div
        ref={ref}
        role='status'
        aria-busy='true'
        aria-live='polite'
        className={classes}
        style={hasInlineStyle ? mergedStyle : undefined}
        {...rest}
      >
        {isMultilineText &&
          Array.from({ length: lines as number }, (_, i) => (
            <span key={i} className='skeleton__line' aria-hidden='true' />
          ))}
        {label !== undefined && <span className='sr-only'>{label}</span>}
      </div>
    );
  }
);
Skeleton.displayName = 'Skeleton';
```

## Source: skeleton.css

```css
.skeleton {
  display: block;
  width: 100%;
  height: 16px;
  background: var(--background-tertiary);
  background-image: linear-gradient(
    90deg,
    var(--background-tertiary) 0%,
    var(--background-quaternary) 50%,
    var(--background-tertiary) 100%
  );
  background-size: 200% 100%;
  animation: skeleton-shimmer 1.6s ease-in-out infinite;
  border-radius: 2px;
}
.skeleton--circle {
  width: 40px;
  height: 40px;
  border-radius: 9999px;
}
.skeleton--text {
  height: auto;
  min-height: 16px;
  background: transparent;
  animation: none;
  display: flex;
  flex-direction: column;
  gap: 8px;
  border-radius: 0;
}
.skeleton__line {
  display: block;
  height: 12px;
  background: var(--background-tertiary);
  background-image: linear-gradient(
    90deg,
    var(--background-tertiary) 0%,
    var(--background-quaternary) 50%,
    var(--background-tertiary) 100%
  );
  background-size: 200% 100%;
  animation: skeleton-shimmer 1.6s ease-in-out infinite;
  border-radius: 2px;
}
.skeleton__line:last-child:not(:only-child) {
  width: 65%;
}
@keyframes skeleton-shimmer {
  0% {
    background-position: 200% 0;
  }
  100% {
    background-position: -200% 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .skeleton,
  .skeleton__line {
    animation: none;
  }
}
```

## HTML / vanilla variant

```html
<span class="skeleton skeleton--circle"></span>
<span class="skeleton skeleton--text" style="width:80%"></span>
```

Interactive behaviours for plain HTML come from the vanilla runtime (data-uikit-* attributes): https://design.freecodecamp.org/registry/vanilla.md - or download https://design.freecodecamp.org/cdn/uikit.global.js once and self-host it (do not hotlink).

## For coding agents

This library is distributed as copyable source, not an npm package. Start at https://design.freecodecamp.org/registry/starter.md, discover components via https://design.freecodecamp.org/llms.txt, and copy files into the consuming project. Keep token names intact; recolour by editing the copied tokens.css.
