UILoaders and Feedbackno dependencies

SkeletonShimmer

Content placeholder blocks — an avatar, title lines, and body lines — with a soft shimmer band sweeping across every bar in sync.

Preview

Customize

7 props
3
1.6
6
360

Installation

Copies the component source and its CSS Module into your project.

npx motionfoundry add skeleton-shimmer

Writes components/ui/skeleton-shimmer.tsx and components/styles/skeleton-shimmer.module.css.

Usage

Updates live as you change the controls — only props that differ from the defaults are included.

example.tsx
1import { SkeletonShimmer } from "@motionfoundry/ui";2 3export function Example() {4  return (5    <SkeletonShimmer />6  );7}

Props

PropTypeDefault
lines

Number of body lines (0–7).

number3
avatar

Show the avatar and title block.

booleantrue
avatarShape

Avatar shape.

'rounded' | 'circle''rounded'
card

Wrap in a bordered card.

booleantrue
speed

Seconds per shimmer sweep.

number1.6
baseColor

Placeholder block color.

stringtheme raised surface
shimmerColor

Shimmer highlight color.

string'rgba(255, 255, 255, 0.09)'
radius

Bar corner radius in pixels.

number6
width

Width in pixels.

number360
label

Accessible loading label.

string'Loading content'
className

Merged onto the root.

string''
style

Merged onto the root.

React.CSSPropertiesundefined

Accessibility

The wrapper is role="status" with aria-busy and a label (default "Loading content"); every bar is aria-hidden. Widths are deterministic so SSR markup matches the client. Under prefers-reduced-motion the shimmer stops and plain blocks remain.

Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.

Source

components/ui/skeleton-shimmer.tsx
1"use client";2 3import * as React from "react";4import styles from "../styles/skeleton-shimmer.module.css";5 6export interface SkeletonShimmerProps {7  /** Number of body lines */8  lines?: number;9  /** Show the avatar + title block */10  avatar?: boolean;11  /** Avatar shape */12  avatarShape?: "rounded" | "circle";13  /** Wrap the placeholder in a bordered card */14  card?: boolean;15  /** Seconds per shimmer sweep */16  speed?: number;17  /** Placeholder block color (defaults to the theme's raised surface) */18  baseColor?: string;19  /** Shimmer highlight color */20  shimmerColor?: string;21  /** Corner radius of the bars in pixels */22  radius?: number;23  /** Width in pixels */24  width?: number;25  /** Accessible loading label */26  label?: string;27  /** Optional className merged onto the root */28  className?: string;29  /** Optional inline styles merged onto the root */30  style?: React.CSSProperties;31}32 33// Deterministic widths so server and client markup match.34const LINE_WIDTHS = ["100%", "100%", "62%", "88%", "74%", "94%", "56%"];35 36export function SkeletonShimmer({37  lines = 3,38  avatar = true,39  avatarShape = "rounded",40  card = true,41  speed = 1.6,42  baseColor,43  shimmerColor = "rgba(255, 255, 255, 0.09)",44  radius = 6,45  width = 360,46  label = "Loading content",47  className = "",48  style,49}: SkeletonShimmerProps) {50  const count = Math.max(0, Math.min(7, Math.round(lines)));51  const widths = Array.from({ length: count }, (_, i) => (i === count - 1 && count > 1 ? "58%" : LINE_WIDTHS[i]));52 53  return (54    <div55      role="status"56      aria-busy="true"57      aria-label={label}58      className={`${styles.root} ${card ? styles.card : ""} ${className}`.trim()}59      style={60        {61          "--mf-sk-speed": `${Math.max(0.4, speed)}s`,62          "--mf-sk-shine": shimmerColor,63          "--mf-sk-radius": `${radius}px`,64          "--mf-sk-width": `${width}px`,65          ...(baseColor ? { "--mf-sk-base": baseColor } : null),66          ...style,67        } as React.CSSProperties68      }69    >70      {avatar && (71        <div className={styles.head} aria-hidden="true">72          <span className={`${styles.bar} ${styles.avatar} ${avatarShape === "circle" ? styles.circle : ""}`.trim()} />73          <span className={styles.titles}>74            <span className={styles.bar} style={{ width: "72%" }} />75            <span className={styles.bar} style={{ width: "44%" }} />76          </span>77        </div>78      )}79      {count > 0 && (80        <div className={styles.body} aria-hidden="true">81          {widths.map((w, i) => (82            <span key={i} className={styles.bar} style={{ width: w }} />83          ))}84        </div>85      )}86    </div>87  );88}