Text-Aware Masonry Cards
Pack cards into a masonry grid by predicting text heights before rendering.
Masonry grids need to know card heights before positioning them. When cards contain text, that height depends on the content, font, and available width. Pretext makes this a computation instead of a measurement.
prepare()layout() What this demonstrates
Using Pretext to predict the height of each card's text content, then packing cards into columns using a shortest-column-first algorithm. All dimensions are known before any card is rendered, eliminating layout shift.
Relevant Pretext API
prepare(text, font)— one call per card textlayout(prepared, width, lineHeight)— get height for packing
Why this is hard with the DOM
Traditional masonry grids render cards first, then read their heights, then reposition them. This causes visible layout shift and requires multiple DOM operations. With Pretext, the grid can be fully computed before any element enters the DOM.
Quick start
import { prepare, layout } from '@chenglou/pretext';
// Card data — each has variable-length text content
const cardTexts = [
'Short card.',
'A medium card with more text to show height variation.',
'This card has significantly more content to demonstrate ' +
'how Pretext predicts exact height for masonry packing.',
// ... more cards
];
const font = '13px Inter, sans-serif';
const lineHeight = 13 * 1.55; // ~20px
const cardWidth = 240;
const innerPadding = 20; // padding inside each card
const gap = 12;
const numColumns = 4;
// Step 1: Predict every card's height BEFORE rendering.
// No hidden elements, no trial-and-error, no layout shift.
const cards = cardTexts.map(text => {
const prepared = prepare(text, font);
const textArea = cardWidth - innerPadding * 2; // subtract padding
const result = layout(prepared, textArea, lineHeight);
return {
text,
// Total card height = text height + padding + accent bar + footer
height: result.height + innerPadding * 2 + 8,
lineCount: result.lineCount,
};
});
// Step 2: Shortest-column-first packing algorithm.
// All positions are computed with pure arithmetic.
const colHeights = new Array(numColumns).fill(0);
const positioned = cards.map(card => {
// Find the shortest column
let minCol = 0;
for (let c = 1; c < numColumns; c++) {
if (colHeights[c] < colHeights[minCol]) minCol = c;
}
const top = colHeights[minCol];
const left = minCol * (cardWidth + gap);
colHeights[minCol] += card.height + gap;
return { ...card, top, left, col: minCol };
});
// Step 3: Render with absolute positioning — zero layout shift.
// Container height is known before any DOM element is created.
const containerHeight = Math.max(...colHeights);
positioned.forEach(card => {
const el = document.createElement('div');
el.style.cssText = `position:absolute;top:${card.top}px;` +
`left:${card.left}px;width:${card.cardWidth}px;height:${card.height}px`;
el.textContent = card.text;
container.appendChild(el);
});