React layout library implementing Hermitage-style 2D bin-packing for picture arrangement
npm install petersburgA React layout library implementing Hermitage-style 2D bin-packing for picture arrangement.
Named after the Hermitage Museum in St. Petersburg, where curators arrange paintings tetris-style to maximize limited wall space.
- MaxRects bin-packing algorithm — efficient 2D rectangle packing
- Multiple sort strategies — optimize for packing efficiency or preserve input order
- Auto-height measurement — items can omit height; Petersburg measures rendered content
- Responsive — auto-measures container and recalculates on resize
- Accessible — DOM order matches visual flow for proper tab navigation
- Lightweight — no dependencies beyond React
``bash`
npm install petersburg
`tsx
import { HermitageLayout } from 'petersburg';
const items = [
{ id: '1', width: 200, height: 150, content: },
{ id: '2', width: 100, height: 100, content: },
{ id: '3', width: 150, height: 200, content: },
];
function Gallery() {
return (
gap={8}
sortStrategy="ordered"
/>
);
}
`
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| items | LayoutItem[] | required | Array of items to layout |containerWidth
| | number | auto | Fixed width in pixels. If omitted, measures parent container. |gap
| | number | 8 | Gap between items in pixels |sortStrategy
| | SortStrategy | "none" | How to order items during packing |className
| | string | — | CSS class for the container |
`tsx
interface LayoutItem {
id: string;
width: number;
height?: number; // Optional — if omitted, auto-measured from content
content: ReactNode;
}
type SortStrategy =
| "none" // Keep input order
| "height-desc" // Sort by height descending (best packing)
| "ordered"; // Row 1 strict order, row 2+ flexible
`
This is ideal for "newest items at top" layouts where the first row should show items 1, 2, 3... in order, but lower rows can be optimized.
For items with dynamic content (text cards, variable-length descriptions), omit the height property:
`tsx
const items = [
{ id: '1', width: 200, content:
{ id: '2', width: 200, content:
{ id: '3', width: 150, content:
];
`
Petersburg will:
1. Render items invisibly to measure their natural height
2. Pack using the measured dimensions
3. Display the final layout
This adds a brief measurement phase but avoids content overflow or fixed-height constraints.
Omit containerWidth to enable responsive mode:
`tsx`
The component will measure its parent container and recalculate the layout when the container resizes.
For fixed-width layouts, provide containerWidth:
`tsx`
containerWidth={800}
gap={8}
/>
Each item is rendered in an absolutely-positioned wrapper. Style your content to fill it:
`tsx`
const items = [
{
id: '1',
width: 200,
height: 150,
content: (
src="..."
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
),
},
];
Petersburg intentionally doesn't include animations to stay lightweight. Add your own with CSS transitions:
`css``
.my-gallery img {
transition: transform 0.3s ease, opacity 0.3s ease;
}
Or use your preferred animation library on the item content.
Petersburg uses the MaxRects bin-packing algorithm:
1. Start with the full container as free space
2. For each item, find the best position (topmost, then leftmost)
3. Place the item and split the remaining free space into new rectangles
4. Prune redundant free rectangles
5. Repeat until all items are placed
This produces efficient layouts where items fill gaps left by differently-sized neighbors.
MIT