メーソンリー
stimeo--masonry
高さの異なるカードを最短列へ敷き詰めつつ、DOM 順を保つ。
stimeo--masonry コントローラには専用の APG パターンはなく、レイアウト補助に徹する。コンテナ幅と min-column-width から応答的な列数を算定し、各アイテムをそのとき最も低い列へ配置する。列数は --stimeo-masonry-columns に、各アイテムの列 index は data-column に公開する。ResizeObserver と MutationObserver がリサイズ時・アイテム増減時にレイアウトを再実行する。重要なのは DOM 順を一切変えない点で、読み上げ順・フォーカス順は元の順序のまま保たれる(WCAG 1.3.2)。視覚的な並び順に意味のない独立カード群にのみ使うこと。このデモでは --stimeo-masonry-columns を CSS 多段組(columns)に流し込んでいる。ライブラリは振る舞いのみを提供する。
メモ
短いカード。
変更履歴
段落を増やして列の高さに差をつけた、背の高いカード。最短列への配置が見て取れる。
段落を増やして列の高さに差をつけた、背の高いカード。最短列への配置が見て取れる。
リリースノート
高さに変化をつけるため、補足の文章を数行加えた中くらいのカード。
メモ
短いカード。
変更履歴
段落を増やして列の高さに差をつけた、背の高いカード。最短列への配置が見て取れる。
高さに変化をつけるため、補足の文章を数行加えた中くらいのカード。
リリースノート
高さに変化をつけるため、補足の文章を数行加えた中くらいのカード。
<%# 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 on every relayout. 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));
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。 共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--masonry"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
item
必須
|
data-column付与で最短列へ配置されるカード(DOM順は変えない)。 |
data-stimeo--masonry-target="item" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
minColumnWidth
|
列数算出に使う最小列幅(px、既定240)。 | data-stimeo--masonry-min-column-width-value |
gap
|
アイテム間の余白(px)。列数と高さ計算に反映される(既定16)。 | data-stimeo--masonry-gap-value |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
layout
|
列数が変わると発火する。detailに列数を含む。 | stimeo--masonry:layout |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
--stimeo-masonry-columns |
コンテナ | 現在の列数。デモの CSS columns が消費する。 |
data-column |
アイテム | そのアイテムが割り当てられた列 index。 |