Content placeholder blocks — an avatar, title lines, and body lines — with a soft shimmer band sweeping across every bar in sync.
Preview
Customize
7 propsInstallation
Copies the component source and its CSS Module into your project.
npx motionfoundry add skeleton-shimmerWrites 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.
1import { SkeletonShimmer } from "@motionfoundry/ui";2 3export function Example() {4 return (5 <SkeletonShimmer />6 );7}Props
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
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}