CSS Grid Lanes — masonry v CSS bez jediného řádku JS

CSS Grid Lanes — masonry v CSS bez jediného řádku JS

Pinterest layout dlouho znamenal Masonry.js nebo CSS columns s rozbitým reading order. display: grid-lanes to mění — nativní masonry s fallbackem přes @supports.

Jakub Kontra
Jakub Kontra
Developer

Shrnutí

Masonry layout — ten Pinterest-style mřížkový rozpad, kde každá karta má jinou výšku a sloupce se zaplňují podle nejkratšího — byl roky frustrace. Buď jsi tahal 30 kB JavaScriptu, nebo jsi sáhl po column-count a smířil se s tím, že čtenář čte shora dolů, ne zleva doprava. CSS Grid Lanes (display: grid-lanes) to konečně řeší přímo v CSS, bez JS, bez resize listenerů a s dvouřádkovým fallbackem přes @supports. Tenhle článek ti ukáže syntaxi, jak to nasadit dneska a kde je to past.


Proč mě to vůbec zajímá

Za posledních deset let jsem masonry řešil snad pětkrát. Pokaždé to dopadlo stejně: Masonry.js, Isotope, později pár ručních requestAnimationFrame skriptů. Každá implementace měla stejný neduh — než se obrázky doloadily, layout poskočil. ResizeObserver pomohl, ale ne úplně. A když pak design system přibalil i SSR, vesměs to skončilo skeleton placeholderama, abychom CLS udrželi pod 0.1.

CSS columns vypadalo jako záchrana, ale není. column-count: 3 zaplní sloupec shora dolů, takže pořadí čtení je: celý první sloupec → celý druhý → celý třetí. Pro chronologickou galerii (Pinterest, blog grid) je to absurdní. Uživatel skenuje řádek, my mu servírujeme sloupec.

Specifikace masonry v CSS se táhne snad od roku 2020. Safari Technology Preview začal s grid-template-rows: masonry a Chromium tým proti tomu podal námitku — že to spojuje dva nezávislé layout módy do jedné property. Pár let se hádali a výsledkem je display: grid-lanes: vlastní display value, čisté oddělení od běžného grid, žádná sémantická skrytá past.

Jak to funguje

Základní syntaxe je tak triviální, že ji odrecituješ ze spánku:

.container {
  display: grid;
  display: grid-lanes; /* podporováno-li, přetíží řádek nad */
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 16px;
}

Tři věci, které stojí za vysvětlení:

Dvojité display: je progressive enhancement zadarmo. Prohlížeč, co grid-lanes nezná, druhý řádek prostě ignoruje a zůstane u grid. Žádné @supports, žádný JS. Nový prohlížeč přepíše hodnotu nahoře a má masonry. Tohle je pro mě nejhezčí věc na celé feature — nepotřebuješ vědět, kdo má jakou verzi.

Lanes = sloupce. Termín "lane" je jen jiné slovo pro to, co znáš jako track v běžném gridu. grid-template-columns definuje, kolik jich bude a jak jsou široké. Tady repeat(auto-fill, minmax(250px, 1fr)) říká: "vejde-li se 250px sloupec, nasázej jich kolik můžeš, a roztáhni je rovnoměrně".

Algoritmus zaplňování. Prohlížeč jde v DOM order položku po položce a každou cpe do nejkratší aktuální lane. Žádné výjimky, žádné optimalizace globální výšky — díky tomu je layout deterministický. Pořadí čtení (DOM) zůstane to, co jsi napsal: prvek N je vždycky před N+1.

Feature detection a fallback

I s dvojitým display: občas chceš pro starší prohlížeče nasadit jiný layout (typicky CSS columns nebo klasický grid). Na to je @supports:

@supports (display: grid-lanes) {
  .gallery {
    display: grid-lanes;
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    gap: 16px;
  }
}

@supports not (display: grid-lanes) {
  .gallery {
    /* fallback — CSS columns nebo klasický grid */
    column-count: 3;
    column-gap: 16px;
  }
  .gallery > * {
    break-inside: avoid;
    margin-bottom: 16px;
  }
}

Co tady stojí za pozornost: break-inside: avoid na potomky. Bez něj ti CSS columns roztrhnou kartu na dva sloupce, a to je přesně ten scénář, který bys nikdy nechtěl ukázat klientovi. Plus margin-bottom místo gapcolumn-gap neřídí svislé mezery uvnitř sloupce.

Pro klienty, co vyžadují IE11 nebo desetiletý Android browser (existují, věřte mi), je tohle absolutní strop. Nic dalšího pro ně dělat nebudu.

Pinterest galerie — kompletní příklad

<ul class="gallery">
  <li><img src="/img/1.jpg" alt="" /></li>
  <li><img src="/img/2.jpg" alt="" /></li>
  <li><img src="/img/3.jpg" alt="" /></li>
  <li><img src="/img/4.jpg" alt="" /></li>
  <li><img src="/img/5.jpg" alt="" /></li>
  <!-- … -->
</ul>
.gallery {
  display: grid;
  display: grid-lanes;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 12px;
  list-style: none;
  padding: 0;
  margin: 0;
}

.gallery img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 8px;
}

To je celé. Žádný JS, žádný ResizeObserver, žádné poskoky při loadu. Obrázek si určí výšku sám podle aspect ratio a grid-lanes ho narve do nejkratší lane. Když přidáš aspect-ratio přímo na <img>, eliminuješ i CLS před stažením obrázků.

Kdy to nepoužít

Masonry je nástroj na vizuální zaplnění prostoru, ne na strukturovaná data. Tři scénáře, kdy se mu vyhni:

  1. Tabulky a dashboardy s metrikami. Pokud potřebuješ, aby řádky byly opravdu řádky (porovnání čísel mezi kartami), masonry ti zarovnání rozhází. Použij klasický grid s grid-auto-rows: 1fr.
  2. Formuláře. Vyplňování má pevný vertikální tok. Masonry tam nedává žádný smysl.
  3. Cokoliv s drag & drop reorderingem. Algoritmus je deterministický, ale pozice prvku se mění podle obsahu sousedů. Reorder UI to dělá nepředvídatelným.

Pravidlo: pokud zalomený řádek (row) nese sémantiku, masonry ne. Galerie, blog grid, dashboard widgetů s nezávislým obsahem — ano. Cokoliv jiného — radši ne.

Závěr

Masonry v CSS je jedna z těch změn, kterým fandím tiše dlouho. Ne kvůli tomu, že by zachraňovala produkty — Masonry.js fungoval. Ale je to jeden balíček navíc, jeden render hook, jeden zdroj poskoků. Tahle feature ho prostě smaže z bundle, a já dostanu zpátky pár kilobajt a hlavně klid v hlavě, že layout počítá engine, ne můj kód.

Pokud děláš galerii, blog grid nebo card layout s nestejnou výškou, nasadit display: grid-lanes s @supports fallbackem je teď otázka pěti minut. A ten fallback bude potřeba ještě tak rok, dva.

Jednou věcí míň, kvůli které tahat JavaScript.