Patterns

  • 1 image: Large, full-width display.
  • 2 images: Side-by-side or split evenly.
  • 3 images: One large image with two smaller ones stacked, or a row of three.
  • 4 images: 2x2 grid.
  • 5+ images: 2x2 grid with an overlay on the last image (e.g., “+3”) to indicate more.

Responsiveness:

  • The layout adapts to screen size using CSS techniques like media queries, fluid grids, and flexible images.
  • Images resize within their containers, maintaining aspect ratio with properties like object-fit: cover
  • For varying image counts, the arrangement and grid structure shift at different breakpoints (e.g., fewer columns on mobile, more on desktop)

Implementation

  • Use CSS Flexbox and media queries to adjust the columns and layout and their breakpoints.

Example Layout Logic

  • For each post, determine the number of images.
  • Apply a layout pattern based on the count (see patterns above).
  • Use CSS flexbox to arrange images accordingly.
  • For each image, ensure it scales responsively and maintains its aspect ratio.
  • For 5+ images, either show a grid with an overlay on the last image.

Example in React

This example uses Styled Components and was built for a project that utilises Strapi CMS for a backend, hence the weird object structure in the data.

import styled from 'styled-components'
 
import PropTypes from 'prop-types'
 
  
 
const MediaLayout = ({ images = [] }) => {
 
// Ensure `images` is always an array
 
const normalizedImages = Array.isArray(images) ? images : []
 
  
 
return (
 
<MediaContainer mediaCount={normalizedImages.length}>
 
{normalizedImages.slice(0, 4).map((img, index) => (
 
<ImageWrapper key={img.id}>
 
<img src={img.attributes?.url} alt={`media ${index + 1}`} />
 
</ImageWrapper>
 
))}
 
{normalizedImages.length > 4 && (
 
<ImageWrapper className="more-images">
 
<div className="overlay">+{normalizedImages.length - 4}</div>
 
<img src={normalizedImages[4].attributes.url} alt="Additional" />
 
</ImageWrapper>
 
)}
 
</MediaContainer>
 
)
 
}
 
  
 
// Validate the `images` prop.
 
MediaLayout.propTypes = {
 
images: PropTypes.arrayOf(
 
PropTypes.shape({
 
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
 
attributes: PropTypes.shape({
 
url: PropTypes.string.isRequired,
 
}),
 
}),
 
),
 
}
 
  
 
MediaLayout.defaultProps = {
 
images: [],
 
}
 
  
 
export default MediaLayout
 
  
 
const MediaContainer = styled.div`
 
display: grid;
 
gap: 0.5rem;
 
margin: 1rem 0;
 
  
 
/* Define grid layout based on the number of images */
 
${({ mediaCount }) => {
 
switch (mediaCount) {
 
case 1:
 
return `
 
grid-template-columns: 1fr;
 
`
 
case 2:
 
return `
 
grid-template-columns: repeat(2, 1fr);
 
`
 
case 3:
 
return `
 
grid-template-columns: 2fr 1fr;
 
grid-template-rows: repeat(2, 1fr);
 
grid-auto-flow: dense;
 
  
 
& > :nth-child(1) {
 
grid-row: span 2;
 
}
 
`
 
case 4:
 
return `
 
grid-template-columns: repeat(2, 1fr);
 
grid-template-rows: repeat(2, 1fr);
 
`
 
default:
 
return `
 
grid-template-columns: repeat(2, 1fr);
 
grid-template-rows: repeat(2, 1fr);
 
  
 
.more-images {
 
position: relative;
 
}
 
  
 
.overlay {
 
position: absolute;
 
top: 0;
 
left: 0;
 
width: 100%;
 
height: 100%;
 
background: rgba(0, 0, 0, 0.5);
 
color: white;
 
font-size: 1.5rem;
 
display: flex;
 
justify-content: center;
 
align-items: center;
 
border-radius: 0.25rem;
 
}
 
`
 
}
 
}}
 
  
 
/* Responsive adjustments */
 
@media (max-width: 768px) {
 
grid-template-columns: 1fr;
 
}
 
`
 
  
 
const ImageWrapper = styled.div`
 
position: relative;
 
  
 
img {
 
width: 100%;
 
height: 100%;
 
object-fit: cover;
 
border-radius: 0.25rem;
 
}
 
`