← Back to Blog

The hard way: Alt Attributes in Gutenberg Blocks

To follow up on the previous post, all that effort to re-establish alt attributes for thousands of images worked out. Mostly. In theory. In practice? We ran into a few SNAFUs actually getting these updated alt attributes to render.

  • For any images we were rendering using php, with the_post_thumbnail() (ref) or custom fields, or standard methodology to grab not only the image source but the image alt attributes, we were fine.
  • For any images that were rendered using the “out of the boxImage block, we were also fine.
  • HOWEVER… we ran into a huge snafu that no one thought about until it was too late when it came to a custom image block.

Out Of The Box - In A Corner

Backing up a bit, we agreed as a development team that the out-of-the-box Gutenberg block for images was a bit overkill for most of our use cases both in Block Patterns and in custom blocks. The site design called for many different usages of images, and initially, we attempted to use the standard Image block.

What we found in practice though, was that the myriad of options that the Image block provides authors (alignment, width / height parameters, aspect ratio, thumbnail sizes, etc, etc) were way too much for our use cases. In most cases, all we really wanted authors to do is tell us which image to load. It seemed not only overwhelming to present to them all the additional options that the Image block provides, considering we likely weren’t going to read any of those values anyway, but also could potentially break the design.

Simplifying The Matter

We decided to register a new custom block, called Simple Image. This interface would allow authors to choose an image, and its thumbnail size, but that’s it.

We built this block entirely in React (saavy readers where see where this is going) and were careful in our block attributes to define the image’s alt attribute when the author chose an image:

function handleImageSelect(image) {
  setAttributes({
    isSVG: image.mime === 'image/svg+xml',
    size: 'large',
    imageId: image?.id || '',
    url: image?.media_details?.sizes[size]?.url || image?.url || '',
    alt: image?.alt || '',
    width: image?.media_details?.sizes[size]?.width || image?.media_details?.width,
    height: image?.media_details?.sizes[size]?.height || image?.media_details?.height,
  });
}

This function fires when the author selects the image in the block.

In our save.js file, we simply read these attributes to render the image in an unopinionated way, so the surrounding block or pattern can style is as needed:

  <figure {...blockProps}>
    <img
      fetchPriority="high"
      decoding="async"
      className={`wp-image-${imageId} simple-image__image ${isSVG && !disableSvgRender ? 'style-svg' : ''}`}
      src={url}
      alt={alt}
      width={width}
      height={height}
    />
  </figure>

The major problem: this only sets the alt attribute when an author selects an image. Since that occurs client-side, the behind-the-scenes behavior to save this data in the database would not automatically apply again, just because the alt attribute is different!

This means for all the thousands of alt attributes we added, hundreds weren’t actually being applied, because of the way this block was constructed.

A workaround and enhancements

The way to rectify this manually would require going into every page that had at least one block using a simple image, and re-select the same image and save the post. Nope, wasn’t gonna do that - that would require touching dozens of pages and hundreds of blocks.

So, the next best thing was to modify our block:

First, we fortunately already had an attribute for the alt text and the image’s ID on the block. The idea was to use a combination of react behavior and the WP Rest API to read, and set the alt attribute on load. We could use the image’s ID to fetch the rest of the image’s properties, and put that in a useEffect to apply that change:


// used to load image locally in editor, but also contains the alt attribute - which was being referenced later anyway in the select function
const image = useSelect(
  (select) => (imageId ? select(coreStore).getMedia(imageId, { context: 'view' }) : null),
  [imageId],
);

// we hook against that image to apply the alt text on load and when image changes.
useEffect(() => {
  if (image?.alt_text) {
    setLocalAlt(image.alt_text);
    setAttributes({
      alt: image.alt_text,
    });
  }
}, [image]);

By hooking on the image variable, in the editor, while it loads, this useEffect would be called for each simple image block. If the image is set, it would then read the alt text - FROM THE DATABASE - and re-apply it to the alt text attribute.

This at least meant all we had to do is go in to every page that had the simple image block and load it and save it in the editor.

Another enhancement we made at the same time was to add a text field to allow authors to set the alt text of the image from within the Gutenberg editor. This meant that if there was an image that our previous import did not update the alt text for, the authors could do it right within the editing interface:

// first, defined a stateful variable to track changes to alt
const [localAlt, setLocalAlt] = useState(attributes.alt || '');

We needed to use the apiFetch method against WordPress’ REST API to apply database changes in real time against the image - and also update the block’s attributes, since the image’s alt is changing, too:

function handleAltUpdate(newAlt) {
  setLocalAlt(newAlt);
  setAttributes({
    alt: newAlt,
  });
  // applies the changes to the actual media library item
  if (imageId) {
    apiFetch({
      path: `/wp/v2/media/${imageId}`,
      method: 'POST',
      data: { alt_text: newAlt },
    });
  }
}

In the editor interface, we added a simple text box to allow authors to read and write updates to the Alt text:

<PanelBody>
  <TextControl
    __nextHasNoMarginBottom
    __next40pxDefaultSize
    label="Image Alt Text"
    value={localAlt}
    onChange={(value) => handleAltUpdate(value)}
  />
</PanelBody>

All-in-all, a bit of a hacky fix, but the enhancement and the fact that it worked made it worth it.

The Right Way and Lessons Learned

The right way to do this would be to simply use a .php file to render the block content, instead of using save.js. This would mean that the data would always be “fresh” on the front end, given a simple ID to grab that information. The downside is that changing to a .php file to render that data would require a “block recovery” in the editor to enable editing again. This is the longer term fix, but under a time crunch, our solution works.

This was a hard, but important lesson to learn! Using React to render the front end should be done only if there aren’t dependencies on other data sources within the WordPress ecosystem.