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 , where small means with the typical grid item height, and then manage how many rows each grid item should span. In other words, we compute the smallest integer that when multiplied with the row height exceeds the current grid item’s height , and then tell grid
to make that item span 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