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