Masonry
A masonry layout that balances items across equal-width columns by measured height, with responsive column counts
Installation
- $npx litefy@latest add masonry
- $pnpm dlx litefy@latest add masonry
- $yarn dlx litefy@latest add masonry
- $bun --bun litefy@latest add masonry
Usage
Items are balanced across equal-width columns; the column count responds to the container width by default (1 / sm:2 / lg:3 / xl:4).
Sunrise
Ocean
Desert
Glacier
Meadow
Mountains
Forest
Canyon
Volcano
"use client";import { Masonry } from "@/ui";const cards = [ { title: "Sunrise", height: "h-24", tone: "bg-primary/10" }, { title: "Mountains", height: "h-44", tone: "bg-accent/20" }, { title: "Ocean", height: "h-32", tone: "bg-primary/15" }, { title: "Forest", height: "h-52", tone: "bg-accent/10" }, { title: "Desert", height: "h-28", tone: "bg-primary/20" }, { title: "Canyon", height: "h-40", tone: "bg-accent/15" }, { title: "Glacier", height: "h-36", tone: "bg-primary/10" }, { title: "Volcano", height: "h-48", tone: "bg-accent/25" }, { title: "Meadow", height: "h-28", tone: "bg-primary/15" },];export default function MasonryBasicDemo() { return ( <div className="w-full max-w-md"> <Masonry items={cards} columns={{ base: 2, md: 3 }} getKey={(card) => card.title} renderItem={(card) => ( <div className={`rounded-lg border p-3 ${card.height} ${card.tone}`}> <p className="text-sm font-medium">{card.title}</p> </div> )} /> </div> );}Custom
Pass a fixed columns count and pair with useLoadMore to grow the list while scrolling an inner container.
8 of 24 loaded
"use client";
import * as React from "react";
import { Button, Masonry, useLoadMore } from "@/ui";
interface Photo {
id: number;
title: string;
height: number;
tone: string;
}
const tones = ["bg-primary/10", "bg-accent/15", "bg-primary/20", "bg-accent/10"];
const allPhotos: Photo[] = Array.from({ length: 24 }, (_, i) => ({
id: i + 1,
title: `Photo ${String(i + 1).padStart(2, "0")}`,
height: 72 + ((i * 37) % 96),
tone: tones[i % tones.length],
}));
export default function MasonryCustomDemo() {
const containerRef = React.useRef<HTMLDivElement>(null);
const { visibleCount, hasMore, loadMore, sentinelRef } = useLoadMore({
total: allPhotos.length,
pageSize: 8,
root: containerRef,
rootMargin: "120px",
});
const visible = allPhotos.slice(0, visibleCount);
return (
<div className="flex w-full max-w-md flex-col gap-2">
<div
ref={containerRef}
className="h-72 overflow-y-auto rounded-lg border p-2"
tabIndex={0}
aria-label="Photo gallery"
data-testid="scroll-root"
>
<Masonry
items={visible}
columns={2}
className="gap-2"
getKey={(photo) => photo.id}
renderItem={(photo) => (
<figure
className={`flex items-end rounded-md border p-2 ${photo.tone}`}
style={{ height: photo.height }}
>
<span className="text-xs font-medium">{photo.title}</span>
</figure>
)}
/>
{hasMore && <div ref={sentinelRef} aria-hidden className="h-px" />}
</div>
<div className="flex items-center justify-between gap-2">
<p className="text-xs text-muted-foreground">
{visibleCount} of {allPhotos.length} loaded
</p>
{hasMore && (
<Button variant="outline" onClick={loadMore}>
Load more
</Button>
)}
</div>
</div>
);
}
Nesting
A Masonry nested inside another (anywhere in the React tree) degrades to a static CSS multi-columns layout — no height balancing, columns fill in source order.
API Reference
High-level Components
Masonry
A data-driven masonry layout; async content that grows after mount is not re-measured — give such items a min-height close to their final size.
| Prop | Type | Default | Description |
|---|---|---|---|
items | T[] | - | The data list |
renderItem | (item: T, index: number) => React.ReactNode | - | Renders one cell |
getKey | (item: T, index: number) => React.Key | index | Stable key per item; also drives the internal measurement cache |
columns | number | { base?; sm?; md?; lg?; xl?; 2xl? } | { base: 1, sm: 2, lg: 3, xl: 4 } | Fixed count, or counts resolved against the container width (Tailwind breakpoints: sm 640 / md 768 / lg 1024 / xl 1280 / 2xl 1536) |
className | ClassNameValue | built-in gap-4 (override via className) | Custom classes for the root — built-in gap-4, override to change spacing |
classNames | { column?; item? } — each ClassNameValue | - | Custom classes per part |
styles | { column?; item? } — each React.CSSProperties | - | Inline styles per part |
Statics
| Static | Type | Description |
|---|---|---|
Masonry.Column | MasonryColumn | A single column stack |
Masonry.Item | MasonryItem | A measured cell wrapper |
Composable Components
MasonryColumn
One equal-width column stack (flex min-w-0 flex-col).
| Prop | Type | Description |
|---|---|---|
className | ClassNameValue | Custom CSS class |
...props | React.ComponentProps<"div"> | Supports all native div props |
MasonryItem
The cell wrapper each item renders into (min-w-0).
| Prop | Type | Description |
|---|---|---|
className | ClassNameValue | Custom CSS class |
...props | React.ComponentProps<"div"> | Supports all native div props |