FullBleedTiles
A grid of tiles running to the edges of the window, ruled by hairlines that are gaps rather than borders. Tiles, and the page in the frame below is built with it.
Scroll inside the frame. Open it on its own for the full width.
The properties are the interface
Override these in your own stylesheet rather than editing the file, so the next version is a clean replacement.
-
--tiles-columns4 - How many tiles across. The rows follow from the ratio rather than from a height, so this is the only number a breakpoint has to change.
-
--tiles-rule1px - The hairline between tiles. It is a gap rather than a border, so there is exactly one of it between any two cells and it cannot be doubled.
-
--tiles-rule-colourrgba(54, 54, 48, 0.28) - The ground behind the gaps, which is what you see as the rules. The tiles cover everything else, so this colour only ever shows in the lines themselves.
-
--tiles-ratio5 / 4 - The shape of one tile. Rows size themselves from this, which is what lets the grid reflow without a breakpoint for every window shape.
-
--tiles-frame1px - The outer line. A grid with no margin has its outermost cells against the trim, so whether there is a rule there is a decision: 0 lets the grid bleed off the page.
-
--tiles-padclamp(0.9rem, 2cqw, 1.8rem) - Padding inside a tile, in container units so it answers to the tile rather than to the window. The same grid then behaves in a sidebar and across a page.
What it gets right
- The rules are the gaps, not borders on the cells. Give every tile a 1px border and the line between two tiles is 2px while the line at the edge is 1px, so the grid reads as slightly broken without anyone being able to say why. A 1px gap with a ground behind it produces exactly one rule everywhere, because there is only ever one gap between two cells
- Cells are sized by ratio rather than by height. A tile grid with fixed pixel rows needs a breakpoint every time the window changes shape; an aspect-ratio means the rows size themselves and a breakpoint only has to say how many columns there are
- The outer rule is a decision rather than an accident. A grid with no margin has its outermost cells against the trim, so tiles-frame draws that line and 0 lets the grid bleed off the page. Most implementations get whichever one the gap happened to give them
- The element is a container, so a tile's padding and type answer to the tile rather than to the window. The same grid in a sidebar and across a page both behave, with no breakpoint needing to know where it was put
- Below 48rem the count drops to two and the tiles become square, because a 5:4 tile two across on a phone is a letterbox with a word in it. A tall tile is reset there too, since a double-height cell beside nothing is not what was meant
The stylesheet
This is the whole item. Copy it into your project, link it, and use the class names. Nothing to install and nothing to mount.
/*
* FullBleedTiles: Beamish
* https://beamish.ink/grids/full-bleed-tiles
*
* A grid of tiles that runs to the edges of the window, ruled by hairlines, with
* no outer margin at all. The kind of front page that is a contents list and a
* composition at the same time.
*
* Three things carry it, and the first is the one everybody gets wrong.
*
* The rules are the gaps, not borders on the cells. Give every tile a 1px
* border and the line between two tiles is 2px while the line at the edge of
* the grid is 1px, so the whole thing reads as slightly broken without anyone
* being able to say why. A 1px gap with a ground behind it produces exactly one
* rule everywhere, because there is only ever one gap between two cells.
*
* The cells are sized by ratio, not by height. A tile grid whose rows are fixed
* pixels needs a breakpoint every time the window changes shape. Giving the
* tile an `aspect-ratio` and letting the column count change means the rows
* size themselves, and the only thing a breakpoint has to say is how many
* columns there are.
*
* And the outer rule is a decision, not an accident. A grid with no margin has
* its outermost cells against the trim, so whether there is a line there at all
* is something you choose: `--tiles-frame` draws it, and at 0 the grid bleeds
* off the page. Most implementations get whichever one the gap happened to give
* them.
*/
:root {
/* How many tiles across. The rows follow from the ratio. */
--tiles-columns: 4;
/* The hairline. One value: it is a gap, so it cannot be doubled. */
--tiles-rule: 1px;
--tiles-rule-colour: rgba(54, 54, 48, 0.28);
/* The shape of one tile. Rows size themselves from this. */
--tiles-ratio: 5 / 4;
/* The outer line, if you want one. 0 bleeds off the page. */
--tiles-frame: 1px;
--tiles-pad: clamp(0.9rem, 2cqw, 1.8rem);
}
/*
* The ground behind the gaps is what you actually see as the rules, so the
* element's background is the line colour and the tiles cover everything else.
*/
.g-tiles {
display: grid;
grid-template-columns: repeat(var(--tiles-columns), minmax(0, 1fr));
gap: var(--tiles-rule);
background: var(--tiles-rule-colour);
border: var(--tiles-frame) solid var(--tiles-rule-colour);
box-sizing: border-box;
width: 100%;
/*
* Container queries, so a tile's own padding and type can answer to the tile
* rather than to the window. The same grid in a sidebar and across a page
* then behaves, with no breakpoint knowing where it was put.
*/
container-type: inline-size;
}
.g-tile {
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
aspect-ratio: var(--tiles-ratio);
padding: var(--tiles-pad);
box-sizing: border-box;
background: var(--background-elevated, #faf9f6);
color: inherit;
text-decoration: none;
overflow: hidden;
}
/*
* A tile that holds a picture and nothing else. The image fills it and the
* ratio still comes from the grid, so one row cannot be a different height
* from the one beside it.
*/
.g-tile--plate {
padding: 0;
}
.g-tile--plate > img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
/* Spans, as grid-column-end, for the tile that is worth two. */
.g-tile--wide {
grid-column-end: span 2;
aspect-ratio: calc(var(--tiles-ratio) * 2);
}
.g-tile--tall {
grid-row-end: span 2;
aspect-ratio: calc(var(--tiles-ratio) / 2);
}
/*
* Narrow. The count drops and the tiles get squarer, because a 5:4 tile two
* across on a phone is a letterbox with a word in it.
*/
@media (max-width: 48rem) {
:root {
--tiles-columns: 2;
--tiles-ratio: 1 / 1;
}
/*
* A span of two on a grid of two is the whole row, which is usually what was
* meant, but a tall tile next to nothing is not. Both are reset.
*/
.g-tile--tall {
grid-row-end: auto;
aspect-ratio: var(--tiles-ratio);
}
}