// Copyright (c) Meta Platforms, Inc. and affiliates. /** * @file Skeleton.tsx * @input Uses React, StyleX keyframes and tokens * @output Exports Skeleton component, SkeletonProps, SkeletonRadius types * @position Core implementation of skeleton loading placeholder * * SYNC: When modified, update these files to stay in sync: * - /packages/core/src/Skeleton/Skeleton.doc.mjs * - /packages/core/src/Skeleton/index.ts * - /apps/storybook/stories/Skeleton.stories.tsx * - /packages/cli/assets/templates/blocks/components/Skeleton/ (showcase blocks) */ import type { BaseProps } from "../BaseProps.js"; import * as stylex from "../style-runtime.js"; import "../theme/tokens.stylex.js"; import { colorVars, radiusVars, durationVars } from "../theme/tokens.stylex.js"; import { mergeProps } from "../utils/index.js"; import { themeProps } from "../utils/themeProps.js"; // ============================================================================= // Animation Timing Constants // ============================================================================= /** * Initial delay before animation starts (ms). * Prevents flash of animation for fast-loading content. */ const DELAY_TIME = 1000; /** * Stagger increment between sequential skeleton elements (ms). * Creates a wave effect when multiple skeletons are used together. */ const STAGGER_TIME = 100; // ============================================================================= // Animation Keyframes // ============================================================================= const skeletonFade = "xgnty7z-B"; // ============================================================================= // Styles // ============================================================================= const styles = { root: { kWkggS: "xprdx6m x1j0df82 x1o55y2u", kSiTet: "xvpkmg4 x1tbh5bv", $$css: true as const }, animate: { kILWW9: "xpz12be", k44tkh: "xqgcaz", ko0y90: "xa4qsjk", kKVMdj: "xeuoslp x1aquc0h", kyAemX: "x193epu2", $$css: true as const } }; const radiusStyles = { "0": { kaIpWk: "xwksslw", $$css: true as const }, "1": { kaIpWk: "xx3sua9", $$css: true as const }, "2": { kaIpWk: "xh6dtrn", $$css: true as const }, "3": { kaIpWk: "x1hviunn", $$css: true as const }, "4": { kaIpWk: "x1hviunn", $$css: true as const }, none: { kaIpWk: "x2u8bby", $$css: true as const }, rounded: { kaIpWk: "xjspbzw", $$css: true as const } }; const _temp = { kKxzle: "xvo38ju", "$$css": true }; const dynamicStyles = { animationDelay: (index: number) => [_temp, { "--x-animationDelay": (val => typeof val === "number" ? val + "ms" : val != null ? val : undefined)(`${DELAY_TIME + STAGGER_TIME * index}ms`) }], dimensions: (width: number | string, height: number | string) => [{ kzqmXN: (typeof width === 'number' ? `${width}px` : width) != null ? "x5lhr3w" : typeof width === 'number' ? `${width}px` : width, kZKoxP: (typeof height === 'number' ? `${height}px` : height) != null ? "x16ye13r" : typeof height === 'number' ? `${height}px` : height, $$css: true as const }, { "--x-width": (val => typeof val === "number" ? val + "px" : val != null ? val : undefined)(typeof width === 'number' ? `${width}px` : width), "--x-height": (val => typeof val === "number" ? val + "px" : val != null ? val : undefined)(typeof height === 'number' ? `${height}px` : height) }] }; // ============================================================================= // Types // ============================================================================= export type SkeletonRadius = keyof typeof radiusStyles | 0 | 1 | 2 | 3 | 4; // ============================================================================= // Component // ============================================================================= export interface SkeletonProps extends BaseProps { /** Ref forwarded to the root element */ ref?: React.Ref; /** * Width of the skeleton. * Accepts a number (pixels) or string (any CSS value). * @default '100%' */ width?: number | string; /** * Height of the skeleton. * Accepts a number (pixels) or string (any CSS value). * @default '100%' */ height?: number | string; /** * Border radius of the skeleton, using design token scale. * - 'none': No border radius (sharp corners) * - 0: radius-0 token (0px) * - 1: radius-1 token (4px) * - 2: radius-2 token (8px) * - 3: radius-3 token (12px, default) * - 4: radius-4 token (16px) * - 'rounded': Fully rounded (for avatars, pills) * @default 3 */ radius?: SkeletonRadius; /** * Index for staggered animation timing. * Use sequential indices (0, 1, 2, ...) for multiple skeletons * to create a wave effect. * @default 0 */ index?: number; /** * Test ID for testing purposes. */ 'data-testid'?: string; } /** * A placeholder shape that indicates content is loading. * Renders a pulsing block with configurable width, height, and border radius. * Use the `index` prop to stagger animation timing across multiple skeletons. * * @example * ``` * * * * * ``` */ export function Skeleton({ width = '100%', height = '100%', radius: radiusProp = 3, index = 0, 'data-testid': testId, xstyle, className, style, ref, ...props }: SkeletonProps) { return