caliper uidocs

Infinite masonry

Block

A masonry grid that loads more as you scroll.

Installs components/ui/infinite-masonry.tsxDepends on @radix-ui/react-slot, lucide-reactMotion none in component source
pnpm dlx shadcn@latest add https://caliperui.site/r/infinite-masonry.json

When 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.