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
JavaScript
// 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