Skip to content

role presentation

Skeleton draft v0.1.0

A placeholder shape that stands in for content while it loads.

A skeleton draws the outline of content that is not there yet: a line of text, an avatar, a card. Several skeletons laid out like the content they stand for tell people what is coming and where, with less jump than a spinner followed by the real content. A skeleton is invisible to assistive technology; the region that loads says so itself.

Web implementation

Root .ds-skeleton on a <span>. Properties map to attributes: shape → data-shape

packages/components/src/components/skeleton/skeleton.css
/*
 * Skeleton — vanilla CSS reference implementation.
 * Spec:   packages/components/specs/components/skeleton.spec.json
 * Tokens: packages/tokens/dist/css/components/skeleton.css (generated from packages/tokens/src/components/skeleton.tokens.json)
 *
 * Rules:
 *  - Only --skeleton-* custom properties may appear here. No raw values, no semantic tokens.
 *  - Shape is a data attribute; a skeleton has no interaction state. It pulses between the
 *    opacity token and fully opaque, at a multiple of the motion duration; still under reduced motion.
 *  - Width and height come from the template's inline style (consumer content), the shape's
 *    default height from its token.
 */

.ds-skeleton {
  --_radius: var(--skeleton-container-border-radius-text);
  --_height: var(--skeleton-container-height-text);

  box-sizing: border-box;
  display: block;
  inline-size: var(--_width, 100%);
  block-size: var(--_height);
  border-radius: var(--_radius);
  background-color: var(--skeleton-container-background);
  animation: ds-skeleton-pulse calc(var(--skeleton-container-animation-duration) * 6) var(--skeleton-container-animation-easing) infinite alternate;
}

.ds-skeleton[data-shape="rectangle"] {
  --_radius: var(--skeleton-container-border-radius-rectangle);
  --_height: var(--skeleton-container-height-rectangle);
}

.ds-skeleton[data-shape="circle"] {
  --_radius: var(--skeleton-container-border-radius-circle);
  --_height: var(--skeleton-container-height-circle);

  inline-size: var(--_width, var(--_height));
  flex: none;
}

@keyframes ds-skeleton-pulse {
  from { opacity: 1; }
  to { opacity: var(--skeleton-container-opacity); }
}

@media (prefers-reduced-motion: reduce) {
  .ds-skeleton {
    animation: none;
  }
}
packages/components/src/components/skeleton/skeleton.template.mjs
/**
 * Skeleton — HTML template for the web implementation.
 * Pure function of the spec properties (packages/components/specs/components/skeleton.spec.json) -> HTML string.
 * A placeholder shape hidden from assistive technology: the region that loads says so itself
 * (aria-busy, a status message), not each shape.
 */
import { escapeAttr } from '../../lib/template-utils.mjs';

export const meta = { id: 'skeleton', selector: '.ds-skeleton' };

const LENGTH = /^\d*\.?\d+(px|rem|em|%|ch|vw)$/;

export function render(props = {}) {
  const { shape = 'text', width = null, height = null, state = null } = props;
  const attrs = ['class="ds-skeleton"', `data-shape="${escapeAttr(shape)}"`, 'aria-hidden="true"'];
  const style = [];
  if (width && LENGTH.test(String(width))) style.push(`--_width: ${width}`);
  if (height && LENGTH.test(String(height))) style.push(`--_height: ${height}`);
  if (style.length) attrs.push(`style="${escapeAttr(style.join('; '))}"`);
  if (state && state !== 'default') attrs.push(`data-state="${escapeAttr(state)}"`);
  return `<span ${attrs.join(' ')}></span>`;
}

No other platform implementation recorded yet. Platform token outputs live under Platforms.