UNPKG

scrimr

Version:

🎲 A simple React shimmer component that displays animated random text while loading. Lightweight alternative to skeleton screens.

1 lines • 5.8 kB
{"version":3,"sources":["../src/Scrimr.tsx","../src/lib/utils.ts"],"sourcesContent":["import React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"./lib/utils\";\n\ninterface ScrimrProps {\n isLoading: boolean;\n children: React.ReactNode;\n length?: number; // visible placeholder length\n speed?: number; // ms\n chars?: string; // character pool\n className?: string;\n placeholderLabel?: string; // a11y label for loading content\n partialUpdateRatio?: number;// 0~1, portion of chars to mutate per tick\n}\n\nconst DEFAULT_CHARS =\n \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&* \";\n\nconst clamp = (n: number, min: number, max: number) =>\n Math.min(max, Math.max(min, n));\n\nconst pick = (chars: string) =>\n chars[Math.floor(Math.random() * chars.length)];\n\n/** mutate only some positions for a smoother effect */\nfunction mutateText(\n text: string,\n chars: string,\n ratio: number\n): string {\n if (!text) return text;\n const arr = text.split(\"\");\n const changes = Math.max(1, Math.floor(arr.length * ratio));\n for (let i = 0; i < changes; i++) {\n const idx = Math.floor(Math.random() * arr.length);\n arr[idx] = pick(chars);\n }\n return arr.join(\"\");\n}\n\nexport const Scrimr: React.FC<ScrimrProps> = ({\n isLoading,\n children,\n length = 20,\n speed = 30,\n chars = DEFAULT_CHARS,\n className,\n placeholderLabel = \"Loading content\",\n partialUpdateRatio = 0.8,\n}) => {\n const safeLength = clamp(Math.floor(length), 1, 5000);\n const hasChars = typeof chars === \"string\" && chars.length > 0;\n const pool = hasChars ? chars : DEFAULT_CHARS;\n\n const [text, setText] = useState<string>(\"\");\n const timerRef = useRef<number | ReturnType<typeof setInterval> | undefined>(undefined);\n\n // reduced motion support\n const prefersReducedMotion = useMemo(() => {\n if (typeof window === \"undefined\" || !(\"matchMedia\" in window)) return false;\n return window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n }, []);\n\n useEffect(() => {\n if (!isLoading) {\n if (timerRef.current) {\n clearInterval(timerRef.current as number);\n timerRef.current = undefined;\n }\n return;\n }\n\n // initial text\n setText(Array.from({ length: safeLength }, () => pick(pool)).join(\"\"));\n\n const interval = prefersReducedMotion ? 400 : speed;\n\n const tick = () => {\n // pause when tab hidden to save cycles\n if (typeof document !== \"undefined\" && document.visibilityState === \"hidden\") return;\n setText((prev) => {\n if (!prev) {\n return Array.from({ length: safeLength }, () => pick(pool)).join(\"\");\n }\n return mutateText(prev, pool, clamp(partialUpdateRatio, 0.05, 1));\n });\n };\n\n timerRef.current = setInterval(tick, interval);\n return () => {\n if (timerRef.current) {\n clearInterval(timerRef.current as number);\n timerRef.current = undefined;\n }\n };\n }, [isLoading, safeLength, pool, speed, partialUpdateRatio, prefersReducedMotion]);\n\n if (!isLoading) return <>{children}</>;\n\n return (\n <span\n className={cn(\n \"block animate-pulse text-gray-400 select-none font-mono truncate\",\n className\n )}\n role=\"status\"\n aria-live=\"polite\"\n aria-label={placeholderLabel}\n aria-busy=\"true\"\n >\n <span aria-hidden=\"true\">{text}</span>\n </span>\n );\n};\n\nexport default Scrimr","import { type ClassValue, clsx } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}"],"mappings":"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","names":[]}