Your skeleton screen is lying to your users.
It was drawn to match the card six months ago. Since then the card got a badge, a second line and a bigger avatar. The skeleton didn't. So the page jumps the moment the data arrives.
The usual fix is more work: keep a second layout in sync with the real one, forever. react-loadly 3.1 takes a different route with a new component called Skeletonize. Your real component becomes its own skeleton.
How it works
Wrap the real component and give it placeholder data. While loading is true, Skeletonize renders it as a skeleton:
import { Skeletonize } from "react-loadly";
import "react-loadly/styles.css";
const placeholderUser = { name: "Loading name", role: "Loading role", avatar: "" };
export function Profile() {
const { data, isLoading } = useQuery({ queryKey: ["user"], queryFn: getUser });
return (
<Skeletonize loading={isLoading}>
<UserCard user={data ?? placeholderUser} />
</Skeletonize>
);
}
Text, images, icons and form controls inside UserCard become skeleton blocks with the size and position of the real UI. Containers keep their background, borders and padding. If something else should become a block, add data-skeleton to that element.
Because the skeleton is the real component, there is no second layout to maintain. When someone adds a badge to the card, the skeleton gets the badge too, and there is no layout shift when the data arrives.
What you can tune
Skeletonize takes baseColor, highlightColor and speed, and can inherit them from a SkeletonGroupLoader. You can pick an animation (shimmer, wave, pulse or none) and set loadingLabel for the text screen readers hear.
Accessibility
While loading, the content is inert and aria-hidden. Screen readers hear one role="status" message instead of your placeholder text read out loud. Animations respect prefers-reduced-motion.
Works with the rest of your app
Children keep their state when loading toggles, so hooks and context behave normally. It works with Tailwind, SSR and the Next.js App Router, on React 17, 18 and 19 and Next.js 14, 15 and 16.
The rest of the package
react-loadly also has PulseLoader, SkeletonLoader, SkeletonGroupLoader and SkeletonSwitchLoader, which switches between loading, error, empty and content states.
Version 3.1 also fixes require() for CommonJS projects and makes the package much smaller. I wrote about how those bugs happened in another article.
Try it
npm install react-loadly
Try the live examples, and tell me where it breaks on GitHub.
Top comments (1)
Deriving the skeleton from the real component is a good way to kill layout drift. I'm curious how it handles content whose height depends on data, like a description that wraps to a variable number of lines. Placeholder strings of a fixed length would reserve one size, so you might still get a small shift on arrival. Do you recommend tuning the placeholder text length per field to approximate the average case?