Masonry
stimeo--masonry
Packs cards of different heights into whichever column is shortest, without reordering them.
Packs cards of differing heights into whichever column is currently shortest. How many columns there are follows from the container's width and the narrowest column you allow, and the packing is redone when the width changes or cards come and go. Nothing is reordered: only the visual position changes, while reading order and Tab order stay as your markup wrote them. That makes it right for independent cards rather than a list whose order carries meaning. The column count is readable from CSS, so it can feed a multi-column rule directly.
Quick note
A short card.
Changelog
A taller card whose extra paragraphs make the column heights uneven, so the shortest-column packing is easy to see.
A taller card whose extra paragraphs make the column heights uneven, so the shortest-column packing is easy to see.
Release notes
A medium card with a couple of lines of supporting copy to vary the height.
Quick note
A short card.
Changelog
A taller card whose extra paragraphs make the column heights uneven, so the shortest-column packing is easy to see.
A medium card with a couple of lines of supporting copy to vary the height.
Release notes
A medium card with a couple of lines of supporting copy to vary the height.
Keyboard
This component has no keyboard interactions of its own.
<%# Markup for the masonry (tiled grid) demo.
The library computes the column count from the container width and min-column-width,
and updates --stimeo--masonry-columns and each item's data-column. The look (CSS
multi-column) is the consumer's CSS; reading/focus order stays in DOM order. %>
<div
class="masonry-demo"
data-controller="stimeo--masonry"
data-stimeo--masonry-min-column-width-value="180"
data-stimeo--masonry-gap-value="12">
<div class="masonry-demo__item" data-stimeo--masonry-target="item">
<h3 class="masonry-demo__title"><%= t("components.masonry.demo.cards.short.title") %></h3>
<p><%= t("components.masonry.demo.cards.short.body") %></p>
</div>
<div class="masonry-demo__item" data-stimeo--masonry-target="item">
<h3 class="masonry-demo__title"><%= t("components.masonry.demo.cards.tall.title") %></h3>
<p><%= t("components.masonry.demo.cards.tall.body") %></p>
<p><%= t("components.masonry.demo.cards.tall.body") %></p>
</div>
<div class="masonry-demo__item" data-stimeo--masonry-target="item">
<h3 class="masonry-demo__title"><%= t("components.masonry.demo.cards.medium.title") %></h3>
<p><%= t("components.masonry.demo.cards.medium.body") %></p>
</div>
<div class="masonry-demo__item" data-stimeo--masonry-target="item">
<h3 class="masonry-demo__title"><%= t("components.masonry.demo.cards.short.title") %></h3>
<p><%= t("components.masonry.demo.cards.short.body") %></p>
</div>
<div class="masonry-demo__item" data-stimeo--masonry-target="item">
<h3 class="masonry-demo__title"><%= t("components.masonry.demo.cards.tall.title") %></h3>
<p><%= t("components.masonry.demo.cards.tall.body") %></p>
<p><%= t("components.masonry.demo.cards.medium.body") %></p>
</div>
<div class="masonry-demo__item" data-stimeo--masonry-target="item">
<h3 class="masonry-demo__title"><%= t("components.masonry.demo.cards.medium.title") %></h3>
<p><%= t("components.masonry.demo.cards.medium.body") %></p>
</div>
</div>
<p
class="masonry-demo__status"
data-masonry-status
data-template="<%= t("components.masonry.demo.status_template") %>"
aria-live="polite"></p>
/*
* Presentation-only styles for the masonry demo.
* Feed the library's --stimeo--masonry-columns into CSS multi-column (columns) to pack
* cards of varying heights with no gaps. Column splitting is CSS; reading order stays
* in DOM order.
*/
.masonry-demo {
columns: var(--stimeo--masonry-columns, 1);
column-gap: 12px;
}
.masonry-demo__item {
/* Keep cards from breaking across columns in the multi-column layout. */
break-inside: avoid;
margin: 0 0 12px;
padding: 0.85rem 1rem;
border: 1px solid var(--border-strong);
border-radius: 0.5rem;
background: var(--surface, var(--surface-card));
}
.masonry-demo__title {
margin: 0 0 0.4rem;
font-size: 0.95rem;
font-weight: 600;
color: var(--fg, var(--color-text));
}
.masonry-demo__item p {
margin: 0 0 0.4rem;
font-size: 0.85rem;
line-height: 1.5;
color: var(--color-text-muted);
}
.masonry-demo__status {
margin: 0.75rem 0 0;
font-size: 0.8rem;
color: var(--color-text-muted);
}
// Demo that consumes masonry's state hooks (consumer-side JS).
//
// The core controller (stimeo--masonry) computes the column count, updates
// --stimeo--masonry-columns and each item's data-column, and fires
// stimeo--masonry:layout whenever that published result moves. Here we only subscribe
// to that event and show the current column count in a live region. The layout itself
// is CSS (columns).
//
// For the bilingual catalog the copy isn't hardcoded: JS substitutes the number into
// the localized template the ERB passes (the "{count}" token in data-template).
document.querySelectorAll('[data-controller~="stimeo--masonry"]').forEach((masonry) => {
const status = masonry.parentElement?.querySelector('[data-masonry-status]');
if (!status) return;
const template = status.dataset.template || '{count}';
const render = (columns) => {
status.textContent = template.replace('{count}', String(columns));
};
masonry.addEventListener('stimeo--masonry:layout', (event) => {
render(event.detail.columns);
});
// Reflect the initial state right after connect (read the current value from the CSS variable).
const initial = getComputedStyle(masonry).getPropertyValue('--stimeo--masonry-columns').trim();
if (initial) render(Number(initial));
});
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--masonry"
Targets
| Name | Description | Attribute |
|---|---|---|
item
required
|
A card placed into the shortest column via a data-column index (DOM order is never changed). |
data-stimeo--masonry-target="item" |
Values
| Name | Description | Attribute |
|---|---|---|
minColumnWidth
|
The minimum column width in px used to derive the column count (default 240). | data-stimeo--masonry-min-column-width-value |
gap
|
The gap in px between items, factored into column count and height packing (default 16). | data-stimeo--masonry-gap-value |
Events
| Name | Description | Event |
|---|---|---|
layout
|
Dispatched when the column count changes; detail carries the column count. | stimeo--masonry:layout |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
--stimeo--masonry-columns |
Container | The current column count, consumed by the demo's CSS columns. |
data-column |
Item | The column index the item was assigned to. |