How to use code for art direction on my website’s images?

What do we need to achieve? Let’s outline the goals first. In an ideal scenario, we would have:

  • A native image element with alt text for accessibility.
  • A custom image crop for each screen size, ensuring the right visual focus.
  • A custom point of interest for each image crop to maintain proper art direction.
  • Performance optimization without disturbing the layout.
  • Compatibility with all modern, relevant browsers.
  • Progressive enhancement for backward compatibility.

Trying Different Approaches
Our goal is to create a solution that is fully accessible, enhances the quality of the images, and doesn’t negatively impact performance. Let’s explore a few methods to see what works best.

Method 1: Using Object Position with a Picture Element

We could use a picture element with multiple sources for different crops. By applying object-fit and object-position, we can control how the image fills the available space and set a focus point for each crop. However, a major drawback is that object-position can only be set on the image, not on each source. This limits the ability to customize the focus for each crop.

Method 2: Adding a Resize Event Handler

We could add a resize event handler that detects the window.innerWidth and adjusts the image’s object-position accordingly. However, this solution constantly triggers layout recalculations when resizing the viewport, resulting in poor performance and choppy animations. Not a viable option.

Method 3: Using a Mutation Observer

The Mutation Observer API could help us track when the src attribute of an image changes, allowing us to adjust the object-position. However, while the browser displays a different image, the src property doesn’t reflect this change. The browser’s currentSrc property only updates when queried directly, which causes layout trashing. So this method doesn’t solve the issue either.

Method 4: Querying the currentSrc Property

The currentSrc property holds the correct and updated image URL, but it isn’t trackable through a Mutation Observer. Querying it directly still leads to layout trashing, which is what we want to avoid. Another dead end.

Method 5: Using a Proxy

We could try trapping the currentSrc property with a Proxy, intercepting the image change and adjusting the object-position on the fly. However, browsers don’t expose this property in a way that allows us to trap it effectively, leading us back to the same issue of layout recalculations.

A Better Solution: matchMedia API

Digging deeper, we find the matchMedia API, which allows us to monitor media queries without causing layout recalculations. This API, first introduced by Internet Explorer, can trigger events when a new media query becomes active. By pairing this with object-position, we can apply a custom focus for each crop size. The process is efficient, doesn’t affect performance, and is supported by most modern browsers.

Implementation

Here’s a basic structure using picture and matchMedia:

HTML

<header class="header">
  <picture class="header__image">
    <source srcset="large.jpg" media="(min-width: 800px)" style="object-position: 0% 100%"/>
    <source srcset="medium.jpg" media="(min-width: 530px)" style="object-position: 30% 100%"/>
    <source srcset="small.jpg" media="(max-width: 529px)" style="object-position: 100% 66.66%"/>

    <img src="large.jpg" alt="My boss in a penguin suit" style="object-fit: cover; object-position: 0% 100%"/>
  </picture>

  <div class="header__content">
    <h1>Belgium does not actually exist</h1>
    <p>Studies have proven that Belgium is, in reality, a figment of our collective imagination.</p>
  </div>
</header>

CSS

.header {
  position: relative;
  overflow: hidden;
}

.header__image {
  height: 100%;
  left: 0;
  position: absolute;
  top: 0;
  width: 100%;
}

.header__image img {
  width: 100%;
  height: 100%;
  transform: translate3d(0, 0, 0); // Fix rendering glitch in Edge
}

.header__content {
  background: rgba(0, 0, 0, .3);
  color: #fff;
  padding: 100px 50px 50px;
  position: relative;
}

In the accompanying JavaScript, we listen for media query changes and update the object-position of the image based on the active source:

JavaScript

const image = document.querySelector('img');
const sourceElements = document.querySelectorAll('source');
const sources = [];

// We populate the sources array with the information we need,
// so we don't have to query the DOM after this.
sourceElements.forEach(source => sources.push({
  src: source.srcset,
  media: source.media,
  style: { objectPosition: source.style.objectPosition }
}));

// This sets the object position from the <source> on the <img> element.
function applySource(source) {
  if (source && source.style && source.style.objectPosition !== undefined) {
    image.style.objectPosition = source.style.objectPosition;
  }
}

// This gets the MediaQueryLists and registers an event listener on them.
function startMediaQueryListeners() {
  sources.forEach(source => window.matchMedia(source.media).addListener(
    result => result.matches && applySource(source)
  ));
}

// Start listening
startMediaQueryListeners()

// And also, trigger the first set of possible matches.
// Since the largest `<source>` is first, we can just bail out on the first match.
applySource(sources.find(source => window.matchMedia(source.media).matches))
Conclusion

Using matchMedia, we can achieve a responsive, accessible solution that applies art direction effectively without negatively impacting performance. It supports multiple resolutions and is compatible with screen readers, offering a modern, efficient approach to managing images in different breakpoints.