Layout
Adaptive tile mosaic
A container with no artwork of its own draws one from up to four member tiles, with the grid tracks following the tile count so the frame is always full.
- css-grid
- css-only
- no-assets
- cards
- layout
<div class="mos-row">
<div class="mos-card">
<div class="mos-grid mos-count-1">
<div class="mos-tile" style="background: linear-gradient(140deg, #3a6ea5, #1d3557);" aria-hidden="true"></div>
</div>
<p class="mos-caption"><strong>Q1 roadmap</strong><br /><span>Phase 1</span></p>
</div>
<div class="mos-card">
<div class="mos-grid mos-count-2">
<div class="mos-tile" style="background: linear-gradient(140deg, #3a6ea5, #1d3557);" aria-hidden="true"></div>
<div class="mos-tile" style="background: linear-gradient(140deg, #b5651d, #6b3410);" aria-hidden="true"></div>
</div>
<p class="mos-caption"><strong>Q2 roadmap</strong><br /><span>Phase 2</span></p>
</div>
<div class="mos-card">
<div class="mos-grid mos-count-3">
<div class="mos-tile mos-tile-tall" style="background: linear-gradient(140deg, #3a6ea5, #1d3557);" aria-hidden="true"></div>
<div class="mos-tile" style="background: linear-gradient(140deg, #b5651d, #6b3410);" aria-hidden="true"></div>
<div class="mos-tile" style="background: linear-gradient(140deg, #2f6f5e, #14342b);" aria-hidden="true"></div>
</div>
<p class="mos-caption"><strong>Q3 roadmap</strong><br /><span>Phase 3</span></p>
</div>
<div class="mos-card">
<div class="mos-grid mos-count-4">
<div class="mos-tile" style="background: linear-gradient(140deg, #3a6ea5, #1d3557);" aria-hidden="true"></div>
<div class="mos-tile" style="background: linear-gradient(140deg, #b5651d, #6b3410);" aria-hidden="true"></div>
<div class="mos-tile" style="background: linear-gradient(140deg, #2f6f5e, #14342b);" aria-hidden="true"></div>
<div class="mos-tile" style="background: linear-gradient(140deg, #6c3483, #38184a);" aria-hidden="true"></div>
</div>
<p class="mos-caption"><strong>Q4 roadmap</strong><br /><span>Phase 4</span></p>
</div>
</div>Colors come from shared theme tokens — --surface, --ink, --border, --accent and friends — so this CSS carries no palette
of its own. Use Runnable file to copy the tokens along with it.
.mos-row { display: flex; flex-wrap: wrap; gap: 1.5rem; }
.mos-card {
width: 200px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
}
.mos-grid {
display: grid;
gap: 2px;
height: 150px;
background: var(--surface-2);
}
/* Tracks follow the tile count: one cell, a side-by-side pair, or a 2x2
block. Three tiles reuse the 2x2 grid rather than switching shape again,
which is what keeps the frame full instead of leaving a hole. */
.mos-count-1 { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr); }
.mos-count-2 { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); grid-template-rows: minmax(0, 1fr); }
.mos-count-3,
.mos-count-4 {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
}
.mos-tile { width: 100%; height: 100%; }
/* At three tiles the first one fills the quadrant a fourth would otherwise
leave empty. A held-open empty quadrant reads as a broken image, not a
deliberate three-tile layout. */
.mos-tile-tall { grid-row: span 2; }
.mos-caption { margin: 0; padding: 0.7rem 0.9rem; }
.mos-caption span { color: var(--dim); font-size: 1rem; }Paste this into an agent to rebuild the pattern from scratch.
Build a cover mosaic for a container that has no artwork of its own: draw one from up to four of its member items, with the grid's own tracks changing shape to match how many members there are so the frame is always full.
Use it for any collection-of-collections screen where a folder, set, or group needs a thumbnail but was never assigned an image directly — a synthesized cover beats a generic folder icon because it is actually representative of what's inside, and beats picking one arbitrary member's image because it doesn't imply that one item is more important than the rest. Skip it once a container is allowed to carry its own uploaded cover; branch on that first; and skip it below a UI size where four quadrants are too small to read as anything but noise (a 32px list-row icon should stay a plain glyph).
The track count changes with the member count, not just the number of cells filled: one member is a single `1fr` cell; two are a `1fr 1fr` row; three and four both use a `1fr 1fr` × `1fr 1fr` grid. At exactly three members, give the first tile `grid-row: span 2` so it fills the quadrant a fourth tile would otherwise occupy — a fixed 2x2 grid rendered with only three tiles leaves one quadrant empty, and an empty quadrant in a cover reads as a broken or half-loaded image, not as an intentional three-item layout.
The one rule that actually matters here: every track has to be `minmax(0, 1fr)`, never a bare `1fr`. A bare `1fr` still carries CSS Grid's automatic minimum of `min-content`, so the moment a tile's content (a real photo, in production) has real intrinsic height, that track refuses to shrink below it — the grid grows past the container's own fixed height, and the tile content spills out over the caption below it. `minmax(0, 1fr)` overrides that automatic minimum explicitly and is the only thing that makes the container's fixed height actually hold.
Because this composes the cover from live layout rather than rendering and caching a composited image file, the mosaic updates for free the moment membership changes — there's no invalidation to trigger, no background regeneration job, and no way for the tiles to go stale relative to the real membership, which is the whole tradeoff against pre-rendering a cover image server-side.
Each tile is decorative relative to the container it represents — the name of the collection sits directly beneath it as real text — so tiles carry empty `alt` text (or `aria-hidden`) rather than a redundant description competing with the caption. Take the surface, border, and radius from theme custom properties; the tile fills themselves are legitimate hardcoded sample data standing in for real member artwork, and are the one part of this pattern that should NOT come from the token set.