UNPKG

scrimr

Version:

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

93 lines (91 loc) • 2.99 kB
// src/Scrimr.tsx import { useEffect, useMemo, useRef, useState } from "react"; // src/lib/utils.ts import { clsx } from "clsx"; import { twMerge } from "tailwind-merge"; function cn(...inputs) { return twMerge(clsx(inputs)); } // src/Scrimr.tsx import { Fragment, jsx } from "react/jsx-runtime"; var DEFAULT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&* "; var clamp = (n, min, max) => Math.min(max, Math.max(min, n)); var pick = (chars) => chars[Math.floor(Math.random() * chars.length)]; function mutateText(text, chars, ratio) { if (!text) return text; const arr = text.split(""); const changes = Math.max(1, Math.floor(arr.length * ratio)); for (let i = 0; i < changes; i++) { const idx = Math.floor(Math.random() * arr.length); arr[idx] = pick(chars); } return arr.join(""); } var Scrimr = ({ isLoading, children, length = 20, speed = 30, chars = DEFAULT_CHARS, className, placeholderLabel = "Loading content", partialUpdateRatio = 0.8 }) => { const safeLength = clamp(Math.floor(length), 1, 5e3); const hasChars = typeof chars === "string" && chars.length > 0; const pool = hasChars ? chars : DEFAULT_CHARS; const [text, setText] = useState(""); const timerRef = useRef(void 0); const prefersReducedMotion = useMemo(() => { if (typeof window === "undefined" || !("matchMedia" in window)) return false; return window.matchMedia("(prefers-reduced-motion: reduce)").matches; }, []); useEffect(() => { if (!isLoading) { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = void 0; } return; } setText(Array.from({ length: safeLength }, () => pick(pool)).join("")); const interval = prefersReducedMotion ? 400 : speed; const tick = () => { if (typeof document !== "undefined" && document.visibilityState === "hidden") return; setText((prev) => { if (!prev) { return Array.from({ length: safeLength }, () => pick(pool)).join(""); } return mutateText(prev, pool, clamp(partialUpdateRatio, 0.05, 1)); }); }; timerRef.current = setInterval(tick, interval); return () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = void 0; } }; }, [isLoading, safeLength, pool, speed, partialUpdateRatio, prefersReducedMotion]); if (!isLoading) return /* @__PURE__ */ jsx(Fragment, { children }); return /* @__PURE__ */ jsx( "span", { className: cn( "block animate-pulse text-gray-400 select-none font-mono truncate", className ), role: "status", "aria-live": "polite", "aria-label": placeholderLabel, "aria-busy": "true", children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: text }) } ); }; var Scrimr_default = Scrimr; export { Scrimr, Scrimr_default as default }; //# sourceMappingURL=index.mjs.map