Infinite masonry
BlockA masonry grid that loads more as you scroll.
pnpm dlx shadcn@latest add https://caliperui.site/r/infinite-masonry.jsonWhen to use #
Use it for
- Image or card walls.
Skip it when
- Order matters.
Installation #
terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/infinite-masonry.json
Adds its 3 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above.
example.tsx
import { useCallback, useState } from "react" import { InfiniteMasonry, type MasonryItem } from "@/components/ui/infinite-masonry" const tones = ["var(--chart-1)", "var(--chart-2)", "var(--chart-3)", "var(--chart-4)", "var(--chart-5)", "var(--chart-6)"]; const makeItems = (from: number, count: number): MasonryItem[] => Array.from({ length: count }, (_, k) => { const i = from + k; const aspect = [0.8, 1.25, 1, 0.66, 1.5, 0.9, 1.1][(i * 5 + (i % 3)) % 7]; return { id: `m${i}`, aspect, extra: 52, content: ( <a href="#masonry" className="grid h-full grid-rows-[1fr_auto] overflow-hidden rounded-overlay border border-border bg-background no-underline outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"> <span aria-hidden="true" style={{ background: `color-mix(in srgb, ${tones[i % 6]} ${28 + (i % 4) * 8}%, var(--surface))` }} /> <span className="grid gap-0.5 px-3 py-2"> <span className="text-sm font-medium">Study {i + 1}</span> <span className="text-xs text-muted-foreground">{["Layout", "Colour", "Type", "Motion"][i % 4]}</span> </span> </a> ), }; }); export function InfiniteMasonryDemo() { const [items, setItems] = useState(() => makeItems(0, 12)); const [loading, setLoading] = useState(false); const loadMore = useCallback(() => { setLoading(true); setTimeout(() => { setItems((list) => [...list, ...makeItems(list.length, 12)]); setLoading(false); }, 700); }, []); return ( <InfiniteMasonry label="Studies" items={items} hasMore={items.length < 36} loading={loading} onLoadMore={loadMore} /> ); }
API reference #
InfiniteMasonry
component
PropTypeDefault
items *readonly MasonryItem[]—
label *string—
minColumnWidthnumber220
hasMoreboolean—
loadingboolean—
onLoadMore(() => void)—
classNamestring—
Accessibility #
It exposes status semantics, with aria-label, aria-busy and aria-hidden kept in step with state. Changes that matter are announced through a live region.
No custom key handling; Tab and the native activation keys do the work.
Motion #
It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.