Component
gallery
Demo
tokens.css,
base.css and gallery.css. No scripts.
Source
Copy both files, or run npx nojsui add gallery.
Browser support
Works across current and earlier versions of every major engine.
| Feature | Baseline | Chrome | Edge | Firefox | Safari | Chrome Android | Firefox Android | Safari iOS |
|---|---|---|---|---|---|---|---|---|
| Grid | widely | 57 | 16 | 52 | 10.1 | 57 | 52 | 10.3 |
| Feature | Baseline | Chrome | Edge | Firefox | Safari | Chrome Android | Firefox Android | Safari iOS |
|---|---|---|---|---|---|---|---|---|
| Grid lanes | limited | — | — | — | 26.4 | — | — | 26.4 |
Usage
A gallery of items with different heights — photos in their own proportions, cards with more or less text — packed so that a short item does not leave a hole beneath it. It is a list of figures, laid out by one CSS declaration.
<ul class="sk-gallery" role="list" aria-label="Recent work">
<li>
<figure class="sk-gallery__item">
<img class="sk-gallery__media" src="harbour.jpg" alt="…" width="800" height="1067">
<figcaption class="sk-gallery__caption">Harbour at dawn</figcaption>
</figure>
</li>
…
</ul>
Give each <img> its width and height attributes. The browser derives the
aspect ratio from them before the image loads, so the lanes are packed once
rather than reshuffled as each image arrives.
How it works
One declaration
The gallery is an ordinary grid with lane-sized columns:
.sk-gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(12rem, 100%), 1fr));
align-items: start;
}
@supports (display: grid-lanes) {
.sk-gallery { display: grid-lanes; }
}
display: grid-lanes keeps the column template and the gap and drops the rows.
Each item goes into whichever lane currently ends nearest the start, so items of
different heights pack together instead of lining up in rows.
The fallback is not a second layout to maintain. An engine without lanes reads
the same template and gets rows, each as tall as its tallest item. align-items: start keeps a short item at its own height at the top of its row, so the leftover
space sits underneath it rather than stretching it into a strangely tall card.
Why not columns
CSS multi-column (columns: 12rem) is the usual pre-lanes way to pack
mixed-height items, and it fills down each column before moving across. The
first few items all land in the first column, so the visual order runs
top-to-bottom while a reader expects it to run across. Lanes fill across, close
to the order a grid would use. Multi-column also splits an item across two
columns unless every item is told not to break.
The lane width is not a theme knob
12rem decides how many lanes fit, which is layout rather than appearance
(ADR 0011). To change it, override grid-template-columns on the gallery.
Keyboard contract
None. A gallery is a layout, not a control. Anything focusable you put in an item, such as a link around the image, keeps its own behaviour.
| Key | Behaviour |
|---|---|
Tab | Moves through focusable content in the items in source order, not visual order — see below |
| Any other key | No effect on the gallery |
Verified manually in VoiceOver and NVDA: not yet — do this before marking the component done, and change this line when you have.
Accessibility notes
It is a list. A screen reader announces the number of items and lets a
reader move between them. Keep the <ul>, and keep role="list" even though it
looks redundant: WebKit can drop the list role from a list styled
list-style: none (WebKit bug 170179), and VoiceOver then announces neither the
list nor its count. Every list in the kit whose markers are hidden carries it for the
same reason.
Visual order can drift from source order. Lanes put each item in the lane
that ends nearest the start, so an item can appear one lane over from where it
would sit in a plain grid. Tab and a screen reader follow the source. The drift
is small — lanes fill across, so an item lands in roughly the place a reader
expects — but it is not zero. If your items are links and their order carries
meaning, such as newest first, check that the packed order still reads
correctly at the widths you support. Never use order to adjust it: that moves
the item visually and leaves the announced order where it was.
Captions name the figures. <figcaption> gives each <figure> its
accessible name. An image inside still needs its own alt: the caption names
the figure, and alt describes the picture.
The demo’s placeholders are aria-hidden. Real images replace them, and must
not keep that attribute.
Degradation
| Feature used | Baseline status | Behavior without it |
|---|---|---|
| Grid | widely | The items are a plain list, stacked one per line. Still readable, in order. |
| Grid lanes | limited | The same template lays the items out in rows, each as tall as its tallest item. Short items leave space beneath them; nothing overlaps or breaks. Safari 26.4+ today. |
In a vertical writing mode, the lanes are horizontal strips and the gallery packs along the block axis. Grid columns follow the inline axis, so this needs nothing extra.
Theming
| Property | Default | Controls |
|---|---|---|
--sk-gallery-gap | --sk-space-md | Space between items, in both directions |
--sk-gallery-bg | --sk-color-surface | Item background |
--sk-gallery-border-color | --sk-color-border | Item border |
--sk-gallery-radius | --sk-radius-lg | Item corner radius |
--sk-gallery-caption-fg | --sk-color-text-subtle | Caption text |
--sk-gallery-media-bg | --sk-color-accent-subtle | Fill behind the media, visible while an image loads |