JR

Pages

Blog Posts

Powered by Algolia
masonry

Masonry Layout with CSS Grid

Nov 14, 20181 min readDesign, Web Dev, Tutorial, JS

This is no longer a good way to implement a modal in React. With the recent introduction of React Hooks, I wrote a new Masonry component that’s simpler and more robust.

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 really. A masonry layout may have 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 really 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:

masonry/index.js
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:

masonry/styles.js
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 your own masonry with the above implementation, just 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
© 2020 - Janosh RiebesellRSSThis site is open source
Powered by