JR

Pages

No results for undefined

Blog Posts

No results for undefined
Powered by Algolia

Masonry Layout with CSS Grid

November 14, 20183 min read

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 if you think about. A proper masonry layout doesn’t have rows and without rows, it’s not a grid. Still it would have been cool if CSS grid had you covered, anyway. At least for now though, that is late 2018, almost 2 years after the question. It still doesn’t.

No matter though. 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.

I took the liberty to rewrite his approach in more modern JS. That shaved another 4 lines, bringing it down to just 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 some trouble. The flex items kept disregarding the parent container width, causing massive overflows. Not pretty and I 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, which is to make the row height hrh_\text{r} relative to the height of a typical grid item hitemh_\text{item} and make each grid item take up n=hr/hitemn = \lceil h_\text{r}/h_\text{item}\rceil rows, is quite easy to implement:

// Masonry.js
import React, { Component } from 'react'

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

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

  state = { spans: [], ref: React.createRef() }

  computeSpans = () => {
    const { rowHeight } = this.props
    const spans = []
    Array.from(this.state.ref.current.children).forEach(child => {
      const span = Math.ceil(child.clientHeight / 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.state.ref} {...this.props}>
        {this.props.children.map((child, i) => (
          <Child key={i} span={this.state.spans[i]}>
            {child}
          </Child>
        ))}
      </Parent>
    )
  }
}

and the styled components:

// 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;
`

There you go. All you need to do, is 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 something 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

You can see the result in action here and here.

© 2018 - Janosh RiebesellThis site is open source
Powered byGatsbyGithubNetlify