Skip to content

Components / Loading

Skeleton

Placeholder lines, optionally with an avatar, shimmering while content loads. The shimmer is a gradient texture whose position moves; pure CSS. Marks itself busy for assistive tech.

Live demo

Live, from the library itself: scroll, hover, navigate away and back. The demo is never gated.

Measured

JavaScript of its own

0 B

CSS 4.8 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.

Browser support

Baseline · widely available

every browser.

Install

One command adds the integration, the base tokens and every component. Then import what you use.

npx astro add moonarc
pnpm astro add moonarc
bunx astro add moonarc
src/pages/index.astro
---
import Skeleton from '@moonarc/core/Skeleton';
---
<Skeleton lines={3} avatar />
Copy it into your project instead (shadcn registry)

Owns the file, no dependency. The registry item also installs the base tokens.

terminal
npx shadcn@latest add https://moonarc.dev/r/skeleton.json

The CLI needs a components.json and the @/* alias, which Setup has. The file lands in src/components/moonarc/.

Source

The whole component. Self-contained styles in a cascade layer so your classes always win; it imports nothing.

Skeleton.astro
---
/**
 * Skeleton — placeholder blocks with a shimmer, zero JS. The shimmer is a
 * gradient texture whose position moves; nothing repaints.
 */
import type { HTMLAttributes } from 'astro/types';

interface Props extends HTMLAttributes<'div'> {
  /** Text lines to draw. */
  lines?: number;
  /** Draw a round avatar beside the lines. */
  avatar?: boolean;
  /** Seconds per shimmer pass. */
  duration?: number;
  /** Corner radius of the blocks, any CSS length. */
  radius?: string;
}

const { lines = 3, avatar = false, duration = 1.6, radius = '0.375rem', class: className, ...rest } = Astro.props;
---

<div class:list={['ma-skeleton', className]} aria-busy="true" aria-live="polite" data-avatar={avatar ? '' : undefined} style={`--ma-sk-dur:${duration}s;--ma-sk-r:${radius}`} {...rest}>
  {avatar && <i class="ma-skeleton__avatar"></i>}
  <div class="ma-skeleton__lines">
    {Array.from({ length: Math.max(1, lines) }, (_, i) => <i class="ma-skeleton__line" style={`--ma-w:${i === lines - 1 && lines > 1 ? 60 : 100 - (i % 3) * 8}%`}></i>)}
  </div>
  <slot />
</div>

<style is:global>
  @layer components {
    :where(.ma-skeleton) {
      display: flex;
      gap: 0.75rem;
      align-items: flex-start;
    }
    :where(.ma-skeleton__lines) {
      flex: 1;
      display: grid;
      gap: 0.5rem;
    }
    :where(.ma-skeleton__line),
    :where(.ma-skeleton__avatar) {
      display: block;
      background-color: var(--ma-edge);
      background-image: linear-gradient(100deg, transparent 30%, var(--ma-shine) 50%, transparent 70%);
      background-size: 200% 100%;
      background-position: 150% 0;
      animation: ma-skeleton var(--ma-sk-dur, 1.6s) var(--ma-ease-in-out) infinite;
    }
    :where(.ma-skeleton__line) {
      height: 0.8em;
      width: var(--ma-w, 100%);
      border-radius: var(--ma-sk-r, 0.375rem);
    }
    :where(.ma-skeleton__avatar) {
      flex: none;
      width: 2.5rem;
      height: 2.5rem;
      border-radius: 50%;
    }
    @media (prefers-reduced-motion: reduce) {
      :where(.ma-skeleton__line),
      :where(.ma-skeleton__avatar) {
        animation: none;
        background-image: none;
      }
    }
  }

  @keyframes ma-skeleton {
    to {
      background-position: -50% 0;
    }
  }
</style>

Props

PropTypeDefaultDescription
linesnumber3Text lines; widths vary and the last is short, like real text.
avatarbooleanfalseA round block beside the lines.
durationnumber1.6Seconds per shimmer pass.
radiusstring'0.375rem'Corner radius of the lines.

Reduced motion

Static blocks, no shimmer.

With ClientRouter

CSS-only; nothing to rebind.

Why it is built this way

Replaces: Skeleton (shadcn) · react-loading-skeleton. See the migration table.

Progress

Loading

A bar that fills to its value when it scrolls into view, with the number counting up beside it, or a ring with the number in the middle.

newly · Chrome 91 · Firefox 128 · Safari 17scroll

Spinner

Loading

An indeterminate arc whose length breathes while it rotates, so it looks like it is working rather than spinning a fixed wedge.

every browseralways

Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown