An <img> wrapper that holds its place in the layout from the first frame and defers its own fetch, for a page with more pictures than the screen: an article body, a product or portfolio grid, a long feed or timeline, a gallery or lightbox thumbnail strip, a list of avatars or logos, a comparison table of screenshots, a chat history with attachments, a blog index with hero thumbnails. Two things it gets right that a hand-written <img loading="lazy"> does not. First, the space: width and height attributes alone compute to aspect-ratio: auto W / H, and auto means the box re-shapes to the real file once it is known -- so a wrong declared size still shifts the page, just later (measured: a box declared 120x90 in a 300px column reserved 300x225 and then jumped to 300x300 when a 64x64 file landed). This emits the explicit aspect-ratio, which held at 300x225 through that same mismatch, and pairs it with object-fit so the picture is cropped rather than stretched into it. Second, the status: onLoad is not enough, because an image the browser had already finished with fires nothing for a handler attached later -- which is exactly hydration and a second visit to a cached URL, so the placeholder sticks on precisely the fastest images. It asks the element instead, and reads naturalWidth rather than complete, because complete is also true after a 404 and true for an element with no src at all. The fallback src is tried once and never retried, so a missing fallback cannot become the onError loop that hangs the tab. Common asks it answers: "lazy load images react", "react lazy image component", "shadcn image component", "shadcn lazy image", "next/image without next", "next image alternative", "image placeholder while loading react", "blurDataURL without next.js", "lqip react", "blur up image react", "image skeleton loader", "prevent layout shift images", "fix CLS images", "cumulative layout shift image react", "reserve space for image before it loads", "aspect ratio image placeholder", "image jumps when it loads", "page jumps as images load", "img onLoad not firing", "onLoad never fires for cached image", "image placeholder stuck on refresh", "react image onLoad hydration", "detect broken image react", "image fallback on error react", "onError infinite loop img", "fallback image react", "broken image placeholder", "img loading lazy react", "intersection observer image loading", "defer offscreen images", "lighthouse defer offscreen images", "lcp image fetchpriority", "eager load above the fold image", "aria-busy image", "decoding async img react", "lazy load below the fold", "image placeholder tailwind", "responsive image aspect ratio tailwind", "gallery thumbnails lazy load", "avatar list lazy load", "画像 遅延読み込み react", "画像 レイアウトシフト 防ぐ", "CLS 画像 対策", "画像 プレースホルダー react", "読み込み中 画像 スケルトン", "next/image を使わず遅延読み込み", "キャッシュ済み画像で onLoad が発火しない". Not the same thing as official shadcn's aspect-ratio, and not a replacement for it, and the gap is measurable rather than a matter of emphasis: that component is 274 bytes today -- a bare re-export of Radix's AspectRatio.Root with a data-slot on it -- and its skeleton is 267 bytes of div with animate-pulse. Neither knows anything about loading, caching or failure, and across all 223,287 bytes of official's registry (65 listed, 62 fetchable, code-block and questionnaire and trace 404 on both style tracks) the strings lazy, naturalWidth, onLoad, onError, IntersectionObserver and fetchpriority are every one of them a zero hit. Official ships the box; nothing there watches the picture arrive. This is the image itself, and needs no wrapper element -- the reserved ratio, the skeleton and the low-resolution preview are all the img's own style, so it drops into the markup the old <img> was in and keeps every selector and grid placement that pointed at it. What it deliberately leaves alone: srcset and sizes (pass them through; picking them is the job of whatever produces your images), generating the tiny preview, and any CDN. Within pulld, image-zoom takes over once a picture is on screen and worth examining, image-comparison puts two side by side, and avatar-stack handles the small round case. useLazyImage is exported for an <img> of your own. Depends only on your cn util -- no packages, no other components.
pnpm dlx shadcn@latest add "https://pulld.pages.dev/r/lazy-image.json"