Im Mai erfuhr ich, dass Firefox Masonry zu CSS-Grid hinzugefügt hat. Masonry-Layouts sind etwas, das ich schon sehr lange selbst von Grund auf machen wollte, aber ich wusste nie, wo ich anfangen sollte. Also habe ich natürlich die Demo überprüft und dann hatte ich eine Erleuchtung, als ich verstand, wie dieses neue vorgeschlagene CSS-Feature funktioniert.
Die Unterstützung ist derzeit offensichtlich auf Firefox beschränkt (und selbst dort nur hinter einem Flag), aber es bot mir dennoch einen ausreichenden Ausgangspunkt für eine JavaScript-Implementierung, die Browser abdecken würde, die derzeit keine Unterstützung haben.
Firefox implementiert Masonry in CSS, indem entweder grid-template-rows (wie im Beispiel) oder grid-template-columns auf den Wert masonry gesetzt wird.
Mein Ansatz war, dies für unterstützende Browser (was derzeit nur Firefox bedeutet) zu nutzen und einen JavaScript-Fallback für den Rest zu erstellen. Schauen wir uns an, wie das am Beispiel eines Bild-Grids funktioniert.
Zuerst das Flag aktivieren
Um dies zu tun, gehen wir in Firefox zu about:config und suchen nach "masonry". Dies bringt uns zu dem Flag layout.css.grid-template-masonry-value.enabled, das wir aktivieren, indem wir seinen Wert von false (dem Standardwert) auf true doppelklicken.

Legen wir mit etwas Markup los
Die HTML-Struktur sieht ungefähr so aus
<section class="grid--masonry">
<img src="black_cat.jpg" alt="black cat" />
<!-- more such images following -->
</section>
Nun wenden wir ein paar Stile an
Das erste, was wir tun, ist, das oberste Element zu einem CSS-Grid-Container zu machen. Als nächstes definieren wir eine maximale Breite für unsere Bilder, sagen wir 10em. Wir möchten auch, dass sich diese Bilder auf den verfügbaren Platz innerhalb der content-box des Grids verkleinern, falls das Viewport zu schmal wird, um ein einzelnes 10em breites Grid-Spalte unterzubringen. Der Wert, den wir tatsächlich setzen, ist also Min(10em, 100%). Da Responsivität heutzutage wichtig ist, beschäftigen wir uns nicht mit einer festen Anzahl von Spalten, sondern verwenden stattdessen auto-fit, so viele Spalten dieser Breite wie wir können.
$w: Min(10em, 100%);
.grid--masonry {
display: grid;
grid-template-columns: repeat(auto-fit, $w);
> * { width: $w; }
}
Beachten Sie, dass wir Min() und nicht min() verwendet haben, um einen Sass-Konflikt zu vermeiden.
Nun, das ist ein Grid!
Allerdings kein sehr schönes, also zwingen wir seinen Inhalt horizontal in die Mitte, fügen dann einen grid-gap und padding hinzu, die beide einem Abstands-Wert ($s) entsprechen. Wir setzen auch einen background, um es für die Augen angenehmer zu machen.
$s: .5em;
/* masonry grid styles */
.grid--masonry {
/* same styles as before */
justify-content: center;
grid-gap: $s;
padding: $s
}
/* prettifying styles */
html { background: #555 }
Nachdem wir das Grid etwas verschönert haben, widmen wir uns der Verschönerung der Grid-Elemente, das sind die Bilder. Wenden wir einen filter an, damit sie alle etwas einheitlicher aussehen, und verleihen ihnen einen kleinen zusätzlichen Reiz mit leicht abgerundeten Ecken und einem box-shadow.
img {
border-radius: 4px;
box-shadow: 2px 2px 5px rgba(#000, .7);
filter: sepia(1);
}
Das Einzige, was wir jetzt noch für Browser tun müssen, die masonry unterstützen, ist, es zu deklarieren.
.grid--masonry {
/* same styles as before */
grid-template-rows: masonry;
}
Obwohl dies in den meisten Browsern nicht funktionieren wird, liefert es in Firefox mit aktiviertem Flag wie oben erklärt das gewünschte Ergebnis.

grid-template-rows: masonry funktioniert in Firefox mit aktiviertem Flag (Demo).Aber was ist mit den anderen Browsern? Dort brauchen wir ein...
JavaScript-Fallback
Um das von JavaScript auszuführende JavaScript des Browsers sparsam zu halten, prüfen wir zuerst, ob sich Elemente mit der Klasse .grid--masonry auf der Seite befinden und ob der Browser den Wert masonry für grid-template-rows verstanden und angewendet hat. Beachten Sie, dass dies ein generischer Ansatz ist, der davon ausgeht, dass wir möglicherweise mehrere solche Grids auf einer Seite haben.
let grids = [...document.querySelectorAll('.grid--masonry')];
if(grids.length && getComputedStyle(grids[0]).gridTemplateRows !== 'masonry') {
console.log('boo, masonry not supported 😭')
}
else console.log('yay, do nothing!')

Wenn die neue Masonry-Funktion nicht unterstützt wird, holen wir uns den row-gap und die Grid-Elemente für jedes Masonry-Grid und legen dann eine Anzahl von Spalten fest (die für jedes Grid anfänglich 0 ist).
let grids = [...document.querySelectorAll('.grid--masonry')];
if(grids.length && getComputedStyle(grids[0]).gridTemplateRows !== 'masonry') {
grids = grids.map(grid => ({
_el: grid,
gap: parseFloat(getComputedStyle(grid).gridRowGap),
items: [...grid.childNodes].filter(c => c.nodeType === 1),
ncol: 0
}));
grids.forEach(grid => console.log(`grid items: ${grid.items.length}; grid gap: ${grid.gap}px`))
}
Beachten Sie, dass wir sicherstellen müssen, dass die Kindknoten Elementknoten sind (was bedeutet, dass sie einen nodeType von 1 haben). Andernfalls können wir Textknoten mit Wagenrückläufen im Array der Elemente erhalten.

Bevor wir weiter fortfahren, müssen wir sicherstellen, dass die Seite geladen ist und sich die Elemente nicht noch bewegen. Sobald wir das erledigt haben, nehmen wir jedes Grid und lesen seine aktuelle Spaltenanzahl aus. Wenn diese von dem Wert abweicht, den wir bereits haben, aktualisieren wir den alten Wert und ordnen die Grid-Elemente neu an.
if(grids.length && getComputedStyle(grids[0]).gridTemplateRows !== 'masonry') {
grids = grids.map(/* same as before */);
function layout() {
grids.forEach(grid => {
/* get the post-resize/ load number of columns */
let ncol = getComputedStyle(grid._el).gridTemplateColumns.split(' ').length;
if(grid.ncol !== ncol) {
grid.ncol = ncol;
console.log('rearrange grid items')
}
});
}
addEventListener('load', e => {
layout(); /* initial load */
addEventListener('resize', layout, false)
}, false);
}
Beachten Sie, dass wir die Funktion layout() sowohl beim anfänglichen Laden als auch bei Größenänderungen aufrufen müssen.

Um die Grid-Elemente neu anzuordnen, besteht der erste Schritt darin, den oberen Rand bei allen zu entfernen (dieser wurde möglicherweise auf einen Wert größer als Null gesetzt, um den Masonry-Effekt vor der aktuellen Größenänderung zu erzielen).
Wenn das Viewport schmal genug ist, dass wir nur eine Spalte haben, sind wir fertig!
Andernfalls überspringen wir die ersten ncol Elemente und durchlaufen den Rest. Für jedes betrachtete Element berechnen wir die Position des unteren Rands des darüber liegenden Elements und die aktuelle Position seines oberen Rands. Dies ermöglicht es uns zu berechnen, wie weit wir es vertikal verschieben müssen, damit sein oberer Rand einen Grid-Abstand unter dem unteren Rand des darüber liegenden Elements liegt.
/* if the number of columns has changed */
if(grid.ncol !== ncol) {
/* update number of columns */
grid.ncol = ncol;
/* revert to initial positioning, no margin */
grid.items.forEach(c => c.style.removeProperty('margin-top'));
/* if we have more than one column */
if(grid.ncol > 1) {
grid.items.slice(ncol).forEach((c, i) => {
let prev_fin = grid.items[i].getBoundingClientRect().bottom /* bottom edge of item above */,
curr_ini = c.getBoundingClientRect().top /* top edge of current item */;
c.style.marginTop = `${prev_fin + grid.gap - curr_ini}px`
})
}
}
Wir haben jetzt eine funktionierende, browserübergreifende Lösung!
Ein paar kleine Verbesserungen
Eine realistischere Struktur
In einem realen Szenario ist es wahrscheinlicher, dass jedes Bild in einem Link zu seiner Vollbildversion eingebettet ist, sodass das große Bild in einem Lightbox geöffnet wird (oder als Fallback dorthin navigiert wird).
<section class='grid--masonry'>
<a href='black_cat_large.jpg'>
<img src='black_cat_small.jpg' alt='black cat'/>
</a>
<!-- and so on, more thumbnails following the first -->
</section>
Das bedeutet, dass wir auch die CSS etwas ändern müssen. Während wir nicht mehr explizit eine width für die Grid-Elemente festlegen müssen – da es sich jetzt um Links handelt – müssen wir align-self: start darauf setzen, da sie sich im Gegensatz zu Bildern standardmäßig über die gesamte Zeilenhöhe erstrecken, was unseren Algorithmus durcheinander bringt.
.grid--masonry > * { align-self: start; }
img {
display: block; /* avoid weird extra space at the bottom */
width: 100%;
/* same styles as before */
}
Das erste Element über das Grid strecken
Wir können auch das erste Element horizontal über das gesamte Grid strecken (was bedeutet, dass wir seine height wahrscheinlich auch begrenzen und sicherstellen sollten, dass das Bild nicht überläuft oder verzerrt wird).
.grid--masonry > :first-child {
grid-column: 1/ -1;
max-height: 29vh;
}
img {
max-height: inherit;
object-fit: cover;
/* same styles as before */
}
Wir müssen dieses gestreckte Element auch ausschließen, indem wir beim Abrufen der Liste der Grid-Elemente einen weiteren Filterkriterium hinzufügen.
grids = grids.map(grid => ({
_el: grid,
gap: parseFloat(getComputedStyle(grid).gridRowGap),
items: [...grid.childNodes].filter(c =>
c.nodeType === 1 &&
+getComputedStyle(c).gridColumnEnd !== -1
),
ncol: 0
}));
Behandlung von Grid-Elementen mit variablen Seitenverhältnissen
Nehmen wir an, wir möchten diese Lösung für etwas wie einen Blog verwenden. Wir behalten exakt das gleiche JS und fast exakt die gleichen Masonry-spezifischen CSS – wir ändern nur die maximale Breite, die eine Spalte haben kann, und lassen die max-height-Beschränkung für das erste Element weg.
Wie aus der nachstehenden Demo ersichtlich ist, funktioniert unsere Lösung auch in diesem Fall, in dem wir ein Grid von Blogbeiträgen haben, perfekt.
Sie können auch die Größe des Viewports ändern, um zu sehen, wie es sich in diesem Fall verhält.
Wenn wir jedoch möchten, dass die Spaltenbreite etwas flexibel ist, zum Beispiel so
$w: minmax(Min(20em, 100%), 1fr)
Dann haben wir ein Problem bei der Größenänderung.
Die sich ändernde Breite der Grid-Elemente in Kombination mit der Tatsache, dass der Textinhalt bei jedem unterschiedlich ist, bedeutet, dass bei Überschreiten eines bestimmten Schwellenwerts möglicherweise eine andere Anzahl von Textzeilen für ein Grid-Element (und damit eine Änderung der height) auftritt, aber nicht für die anderen. Und wenn sich die Anzahl der Spalten nicht ändert, werden die vertikalen Offsets nicht neu berechnet und wir haben entweder Überlappungen oder größere Lücken.
Um dies zu beheben, müssen wir die Offsets auch neu berechnen, wann immer sich die height mindestens eines Elements für das aktuelle Grid ändert. Das bedeutet, wir müssen auch prüfen, ob mehr als null Elemente des aktuellen Grids ihre height geändert haben. Und dann müssen wir diesen Wert am Ende des if-Blocks zurücksetzen, damit wir die Elemente beim nächsten Mal nicht unnötigerweise neu anordnen.
if(grid.ncol !== ncol || grid.mod) {
/* same as before */
grid.mod = 0
}
Okay, aber wie ändern wir diesen grid.mod-Wert? Meine erste Idee war, einen ResizeObserver zu verwenden.
if(grids.length && getComputedStyle(grids[0]).gridTemplateRows !== 'masonry') {
let o = new ResizeObserver(entries => {
entries.forEach(entry => {
grids.find(grid => grid._el === entry.target.parentElement).mod = 1
});
});
/* same as before */
addEventListener('load', e => {
/* same as before */
grids.forEach(grid => { grid.items.forEach(c => o.observe(c)) })
}, false)
}
Dies erledigt die Aufgabe, die Grid-Elemente bei Bedarf neu anzuordnen, auch wenn sich die Anzahl der Grid-Spalten nicht ändert. Aber es macht sogar die if-Bedingung überflüssig!
Das liegt daran, dass grid.mod auf 1 gesetzt wird, wenn sich die height *oder* die width von mindestens einem Element ändert. Die height eines Elements ändert sich aufgrund des Text-Reflows, der durch die Änderung der width verursacht wird. Die Änderung der width erfolgt jedoch bei jeder Größenänderung des Viewports und löst nicht unbedingt eine Änderung der height aus.
Deshalb habe ich mich schließlich dafür entschieden, die vorherigen Elementhöhen zu speichern und bei Größenänderungen zu prüfen, ob sie sich geändert haben, um zu bestimmen, ob grid.mod 0 bleibt oder nicht.
function layout() {
grids.forEach(grid => {
grid.items.forEach(c => {
let new_h = c.getBoundingClientRect().height;
if(new_h !== +c.dataset.h) {
c.dataset.h = new_h;
grid.mod++
}
});
/* same as before */
})
}
Das war's! Wir haben jetzt eine schöne, leichte Lösung. Das minifizierte JavaScript ist unter 800 Bytes, während die rein Masonry-spezifischen Stile unter 300 Bytes liegen.
Aber, aber, aber...
Was ist mit der Browserunterstützung?
Nun, @supports hat zufällig eine bessere Browserunterstützung als jede der hier verwendeten neueren CSS-Funktionen, daher können wir die schönen Dinge darin platzieren und ein grundlegendes, nicht-masonry Grid für nicht unterstützende Browser haben. Diese Version funktioniert bis zurück zu IE9.

Es sieht vielleicht nicht gleich aus, aber es sieht anständig aus und ist perfekt funktionsfähig. Einen Browser zu unterstützen bedeutet nicht, ihm all den visuellen Schnickschnack zu replizieren. Es bedeutet, dass die Seite funktioniert und nicht kaputt oder scheußlich aussieht.
Was ist mit dem Fall ohne JavaScript?
Nun, wir können die schicken Stile nur anwenden, wenn das Wurzelelement eine js-Klasse hat, die wir per JavaScript hinzufügen! Andernfalls erhalten wir ein einfaches Grid, bei dem alle Elemente die gleiche Größe haben.

Ihre Lösung ist sehr schön! Ahh, ich hätte nie zugelassen, dass leerer Inhalt bis jetzt aufkommt, aber Ihre gesamte Präsentation zu sehen war tatsächlich süß wie Kandiszucker :)
Ich liebe das, die meisten Lösungen da draußen fügen einfach einen Stapel zu unserer Abhängigkeitsliste hinzu. Das ist zukunftssicher, einfach und effizient.
Wirklich schöner Ansatz. Das letzte Skript, das ich verwendet habe, verwendete grid-row-end anstelle von margin, um Elemente zu platzieren, so dass das Grid viele Zeilen verwendete. Mit mehr als 50 Bildern erreichten Chrome oder Firefox ein Speicherlimit. Daher war es unmöglich, diese Methode mit Lazy Loading von unendlichen Elementen im Grid zu verwenden.
Eine kleine Verbesserung Ihres Skripts wäre, alle margin-top-Stile auf einem Grid-Element und nicht im DOM anzuwenden, um Reflows zu vermeiden, die durch jede margin-top-Stil-Anwendung entstehen.
Gute Arbeit!
Wenn ein Element zu hoch ist (z. B. indem der Text für "Macaron" 10 Mal dupliziert wird), gibt es ein Problem: Jede Spalte hat die gleiche Anzahl von Elementen, anstatt die leeren Stellen zu füllen. Gibt es eine Lösung dafür?
Hallo! Vielen Dank für diese Entscheidung. Aber wie kann man nicht das erste, sondern sagen wir das zehnte Element auf die volle Breite strecken? Ist das möglich?
@Mikhail
Hier ist, wie Sie ein Kindelement von Masonry auf volle Breite strecken:
.masonry > *:nth-of-type(10) {
grid-column: 1/-1;
}
Hey! Können Sie mir sagen, wie sich diese Methode auf CLS auswirkt? Ich hatte Probleme mit hohem CLS bei der Verwendung von CSS-Spalten oder Desandros Masonry. Nur gelegentlich und auf einigen Seiten bei reinen CSS-Spalten, aber ich kann es dann trotzdem nicht verwenden.
Diese Lösung ist wirklich großartig! Ich habe meine alten Masonry/Packery-Lösungen durch Ihr Skript ersetzt.
Ich habe ein Problem. Die erste Box im Grid erstreckt sich über 3 Spalten. Die beiden Boxen unter dieser Box haben eine große Lücke darüber. Denken über eine Lösung nach. Weiß jemand, wie man das lösen kann?
#grid :first-child {grid-column: 1 / 4;
}