# Skeleton (Moonarc)

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.

- Import: `import Skeleton from '@moonarc/core/Skeleton'`
- Install: `npx astro add moonarc` · copy-paste: `npx shadcn@latest add https://moonarc.dev/r/skeleton.json`
- Tier A · category loading · trigger always
- Readout: `<Skeleton lines={3}>`
- Browser support: widely (every browser)
- Measured cost: 0 B JS (CSS 4.8 kB raw)
- Page: https://moonarc.dev/components/skeleton/

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `lines` | `number` | `3` | Text lines; widths vary and the last is short, like real text. |
| `avatar` | `boolean` | `false` | A round block beside the lines. |
| `duration` | `number` | `1.6` | Seconds per shimmer pass. |
| `radius` | `string` | `'0.375rem'` | Corner radius of the lines. |

## Usage

```astro
<Skeleton lines={3} avatar />
```

## Reduced motion

Static blocks, no shimmer.

## With ClientRouter

CSS-only; nothing to rebind.

## Craft

- Line widths follow a pattern (100 / 92 / 84, last one 60%) so the block reads as text, not as bars.
- The shimmer moves on background-position; ease-in-out gives it a pass-and-rest rhythm rather than a conveyor.
- aria-busy and aria-live: assistive tech is told the region is loading and hears the content when it lands.

## Replaces

- Skeleton (shadcn)
- react-loading-skeleton

## Source

```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>

```
