Gatsby Images with a Fluid Aspect Ratio

Posted by Joseph McGurkin on 3/17/2019

Challenge

Given two images, one portrait and one landscape, render them properly in an image carousel or a masonry layout of Bootstrap cards with image headers.

Requirents

  1. A rendered aspect ratio can be set, no matter what the image's native aspect ratio
  2. Both images should fill the carousel space
  3. Neither image should be distorted with an invalid aspect ratio
  4. The images must be fluid
  5. The entire conatiner or carousel must be fluid
  6. Images must maintain accurate aspect ratios during browser resizing
  7. Clean, simple, reusable

Implementations to Avoid

  1. Edit the images, make them the same size
  2. Control height or width with pixels
  3. Use media breakpoints
  4. JavaScript

TLDR

Drag the middle bar to the left or right. Both images maintain a fluid aspect ratio of 4x3 despite having very different native aspect ratios.

React Component

import React from "react"
import PropTypes from 'prop-types'

const FarImage = (props) => {
    const { aspectRatio, alt, style, ...rest } = props
    let finalStyle = { ...style, ...{ width: "100%" } }
    if (aspectRatio) {
        finalStyle = {
            ...finalStyle,
            ...{ position: "absolute", top: 0, left: 0, objectFit: "cover", height: "100%", width: "100%" }
        }
        return (
            <div style={{ paddingTop: aspectRatio, position: "relative" }}>
                <img alt={alt} style={finalStyle} {...rest} />
            </div>
        )
    }
    else {
        return (
            <img alt={alt} style={finalStyle} {...rest} />
        )
    }
}
FarImage.propTypes = { aspectRatio: PropTypes.string }
FarImage.defaultProps = { aspectRatio: null }

export default FarImage
export const config = {
    Cinemascope: "42.55%",
    Hdtv: "56.25%",
    Ntsc: "75%",
    Widescreen: "54.054%",
}

About the Solution

The React component above is one of my all-time favorite pieces of code. It's extremely simple yet does quite a bit for fluid layouts. So, what's going on? Take the following simple HTML:

<div id="far-wrapper" style="padding-top: 56.25%; position: relative;">
</div>

Read the top padding as "the height is 56.25% of the width." This will render the wrapper div with a 16x9 aspect ratio. Assuming the wrapper's container is 160px wide, the div's height will be 90px. If the div's container is sized to 80px, say by shrinking the browser, the div's padding of 56.25% will force the div to a height of 45px. At this point we have a box which maintains a fluid aspect ratio (FAR) of 16x9, no matter what. To create a 4x3 box, simply use a top padding of 75% instead of 56.25%.

Now place an image inside the FAR wrapper:

<div id="far-wrapper" style="padding-top: 56.25%; position: relative;">
    <img src="..." style="position: absolute; top: 0; left: 0; height: 100%; width: 100%; object-fit: cover;">
</div>

Again, pretty straight forward. Since the div has a top padding of 56.25%, the image would normally be rendered far down on the div. That's why the absolute positioning is added to the image. Move the image up to a top/left of 0/0, and make it 100% of the height and width. The object-fit cover says completely fill that area. The image will be rendered at its native aspect ratio and sized as small as possible while covering the entire bounds of the FAR wrapper.

That's the heavy lifting. With a portrait image, the top and bottom would be cropped and image width would be 100%. With a landscape image, assuming it's wider than 16x9, the left and right would be cropped and the height would be 100%.

The rest of the code in the above React component is only meant to simplify it usage. Adding two FAR images such as below would render both images at the same 16x9 while maintianing a fluid nature.

import FarImage, { config } from "./FarImage"
export default () => {
    return (
        <FarImage aspectRatio={config.Hdtv} src="portraitImage" alt="Sample" />
        <br/>
        <FarImage aspectRatio={config.Hdtv} src="landscapeImage" alt="Sample" />
    )
}

## Next Steps

The demo and source above shows how to maintain a fluid aspect ration. Wrap with a `<picture>` element and multiple sources for various devices.