← home

Masonry Layout with CSS Grid

This is no longer a good way to implement a modal in React.

Back in early January, Wes Bos asked Rachel Andrew the excellent question if CSS grid could be used to produce a masonry layout (think Pinterest). Turns out the spec doesn’t support it. Makes sense since a masonry layout has columns but no rows and without rows, it’s not a grid. Still, it would have been cool if CSS grid had you covered. At least for now though (late 2018, almost 2 years after the question was asked), it doesn’t.

But no matter. That just means we have to get a little creative. And indeed, that’s exactly what Jamie Perkins did. In the above-linked GitHub issue, he presents a neat flexbox solution that requires just 13 lines of JavaScript.

Rewriting his approach in modern JS shaves off another 4 lines, bringing it down to 9:

const numCols = 3
const colHeights = Array(numCols).fill(0)
const container = document.getElementById(`container`)
Array.from(container.children).forEach((child, i) => {
  const order = i % numCols
  child.style.order = order
  colHeights[order] += parseFloat(child.clientHeight)
})
container.style.height = Math.max(...colHeights) + `px`

However, flexbox gave me some trouble. The flex items kept disregarding the parent container width, causing massive overflows. I tried for almost an hour but couldn’t get them to behave. So I wanted to make it happen with grid instead. If you’re using react and styled-components, the suggested solution by Rachel Andrews is quite easy to implement. She proposed to have lots of small rows of height hrh_\text{r}, where small means hrhitemh_\text{r} \ll h_\text{item} with hitemh_\text{item} the typical grid item height, and then manage how many rows each grid item should span. In other words, we compute the smallest integer nn that when multiplied with the row height hrh_\text{r} exceeds the current grid item’s height hitemh_\text{item}, n=hr/hitemn = \lceil h_\text{r}/h_\text{item}\rceil and then tell grid to make that item span nn rows. Here’s the implementation:

import React, { Component, createRef } from 'react'

import { Parent, Child } from './styles'

export default class Masonry extends Component {
  static defaultProps = {
    rowHeight: 40, // in pixels
    colWidth: `17em`,
  }

  state = { spans: [] }
  ref = createRef()
  // sums up the heights of all child nodes for each grid item
  sumUp = (acc, node) => acc + node.scrollHeight

  computeSpans = () => {
    const { rowHeight } = this.props
    const spans = []
    Array.from(this.ref.current.children).forEach(child => {
      const childHeight = Array.from(child.children).reduce(this.sumUp, 0)
      const span = Math.ceil(childHeight / rowHeight)
      spans.push(span + 1)
      child.style.height = span * rowHeight + `px`
    })
    this.setState({ spans })
  }

  componentDidMount() {
    this.computeSpans()
    window.addEventListener('resize', this.computeSpans)
  }

  componentWillUnmount() {
    window.removeEventListener('resize', this.computeSpans)
  }

  render() {
    return (
      <Parent ref={this.ref} {...this.props}>
        {this.props.children.map((child, i) => (
          <Child key={i} span={this.state.spans[i]}>
            {child}
          </Child>
        ))}
      </Parent>
    )
  }
}

and the styled components:

import styled from 'styled-components'

export const Parent = styled.div`
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(${props => props.colWidth}, 1fr));
  grid-auto-rows: calc(${props => props.rowHeight}px - 2em);
  grid-gap: 2em;
`

export const Child = styled.div`
  grid-row: span ${props => props.span};
  height: max-content;
`

To create a masonry with the above implementation, copy the above two files into your project, change the default props for rowHeight and colWidth to suit your needs and use the Masonry component like so:

import React from 'react'

import Masonry from './Masonry'
import PostExcerpt from './PostExcerpt'

const PostList = ({ posts }) => (
  <Masonry>
    {posts.map((post) => (
      <PostExcerpt key={post.slug} {...post} />
    ))}
  </Masonry>
)

export default PostList