Skeleton

Text lines

Avatar + name

Card

Tags

Muted


  
/*
 * Skeleton — foundation
 *
 * Layout placeholder rendered while content loads. CSS-only — uses a
 * shimmer gradient animation. Authors compose multiple skeletons to
 * approximate the final layout.
 *
 *   <div class="skeleton" style="inline-size: 80%;"></div>
 *   <div class="skeleton -circle" style="aspect-ratio: 1;"></div>
 *   <div class="skeleton -rect" style="aspect-ratio: 16/9;"></div>
 *
 * Authors size each skeleton inline because dimensions track the
 * final content layout.
 */

@layer malevich.components {
  .skeleton {
    --skeleton-base: var(--color-surface-raised);
    --skeleton-highlight: var(--color-surface-canvas);
    --skeleton-duration: 1600ms;

    display: block;
    border-radius: var(--radius-button);

    background:
      linear-gradient(
        90deg,
        var(--skeleton-base) 0%,
        var(--skeleton-highlight) 50%,
        var(--skeleton-base) 100%
      );
    background-size: 200% 100%;
    background-repeat: no-repeat;

    animation: malevich-skeleton-shimmer var(--skeleton-duration) ease-in-out infinite;
  }

  .skeleton.-circle {
    border-radius: 50%;
  }

  .skeleton.-rect {
    border-radius: var(--radius-card);
  }

  .skeleton.-pill {
    border-radius: var(--radius-pill);
  }

  .skeleton.-text {
    block-size: var(--font-body-regular-size);
    border-radius: var(--radius-tag);
  }

  .skeleton.-muted {
    --skeleton-base: var(--color-surface-canvas);
    --skeleton-highlight: var(--color-surface-raised);
  }

  @keyframes malevich-skeleton-shimmer {
    0%   { background-position: 100% 0; }
    100% { background-position: -100% 0; }
  }

  @media (prefers-reduced-motion: reduce) {
    .skeleton {
      animation: none;
      background: var(--skeleton-base);
    }
  }
}
This component is pure CSS — no JavaScript required.
# Skeleton — AGENTS.md

> Auto-generated from `skeleton.docs.md` + the modifier applicability matrix.
> Edit those source files; this file regenerates on build.

## Identity

- **Component:** `skeleton`
- **Layer:** foundations
- **Status:** stable
- **Last updated:** 2026-05-18

## Summary

- The page is loading layout-significant content (cards, lists,

## Variants

| Variant      | Class                 | Use for                                  |
|--------------|-----------------------|------------------------------------------|
| Default      | `.skeleton`           | Generic rectangle, button-like radius    |
| Circle       | `.skeleton.-circle`   | Avatars, circular icons                  |
| Rect         | `.skeleton.-rect`     | Cards, large image placeholders          |
| Pill         | `.skeleton.-pill`     | Tags, pill-shaped buttons                |
| Text         | `.skeleton.-text`     | Single line of body text (height fixed)  |
| Muted        | `.skeleton.-muted`    | De-emphasized on raised surfaces         |

Modifiers compose with sizing. Width and height are set inline by the
consumer because they depend on the final layout.

## Modifier applicability

Per ADR-0007. Modifiers attach via `data-{category}="value"` attributes.

| Category | Accepts |
|---|---|
| `background` | — |
| `surface` | — |
| `effect` | — |
| `shader` | — |
| `rhythm` | — |
| `motion` | `none`, `standard` |
| `behavior` | — |

## Anatomy

```html
<!-- Single line of text -->
<div class="skeleton -text" style="inline-size: 80%;"></div>

<!-- Three lines, last one shorter -->
<div class="skeleton -text" style="inline-size: 100%; margin-block-end: 0.5rem;"></div>
<div class="skeleton -text" style="inline-size: 95%; margin-block-end: 0.5rem;"></div>
<div class="skeleton -text" style="inline-size: 60%;"></div>

<!-- Avatar + name pair -->
<div style="display:flex; gap:1rem; align-items:center;">
  <div class="skeleton -circle" style="inline-size: 2.5rem; block-size: 2.5rem;"></div>
  <div style="flex: 1;">
    <div class="skeleton -text" style="inline-size: 40%; margin-block-end: 0.5rem;"></div>
    <div class="skeleton -text" style="inline-size: 25%;"></div>
  </div>
</div>

<!-- Card -->
<div class="skeleton -rect" style="inline-size: 100%; aspect-ratio: 16/9;"></div>
```

## Tokens consumed

### From semantic tier
- `--color-surface-raised` — base color
- `--color-surface-canvas` — highlight color (alternates with base in
  shimmer)
- `--radius-button` — default radius
- `--radius-card` — rect variant radius
- `--radius-pill` — pill variant radius
- `--radius-tag` — text variant radius
- `--font-body-regular-size` — text variant height

### Component-tier (defined inline)
- `--skeleton-base` — overridable base color
- `--skeleton-highlight` — overridable highlight color
- `--skeleton-duration` — overridable shimmer cycle

## Accessibility contract

Skeletons are purely visual placeholders and should not be announced
by screen readers. They lack `role="status"`. The parent container
that swaps skeletons for content should manage announcements (often
via `aria-live` or `aria-busy` on the loading region).

For sustained loading where ARIA feedback matters, pair a skeleton
group with a single `aria-live="polite"` region that announces "Loading"
when loading begins and the loaded content when it arrives.

The component honors `prefers-reduced-motion: reduce` by removing the
shimmer animation and rendering a flat tone.

## Guidance

### Do

- Match the placeholder layout to the final content shape.
- Use `.-text` for any single line of text — the height token keeps
  vertical rhythm stable.
- Group related skeletons (avatar + name lines) so they read as one
  loading unit.

### Don't

- Don't apply skeletons over content the user can already see. Use
  them only for layout that hasn't rendered yet.
- Don't add semantic ARIA roles to skeletons. They are decorative.
- Don't keep skeletons visible for sub-100ms loads. Render directly
  for fast paths and reserve skeletons for waits where the user would
  otherwise see blank space for more than a moment.