Apr 5, 2018 - Abby Milberg

Creating a picture element with multiple fields in D8

Drupal 8 makes it easy to create responsive images via core by using the <picture> element with different image styles. Sometimes, though, you may want to use a different image altogether at different breakpoints, rather than the same image that has just been cropped or sized differently. In these cases, we can use two separate image fields and a pre-process function to create the desired effect.

First, create two fields in your content type. For mine, I used the slugs field_lg_img and field_sm_img. I made the large field required, with required alt text. The small field I left optional, and hid the alt text option since a <picture> element can only take one alt attribute. I also created two image styles with my desired sizes, naming them hero_wide and hero_narrow.

At this point, we're ready for some code. At the top of your my_theme.theme file, make sure you have use Drupal\Core\Url; and use Drupal\image\Entity\ImageStyle;. Next, your preprocess function will look something like this:

 * Implements template_preprocess_node().
function d8_images_preprocess_node__responsive_pic(&$variables) {
  $node = $variables['elements']['#node'];
  // The 'lg_img' field is required, and we get its url with our 'hero_wide'
  // image style.
  $hero_lg_url = ImageStyle::load('hero_wide')->buildUrl($node->field_lg_img->entity->getFileUri());
  // The 'sm_img' field is optional. If it's set, the small url will be the sm_img
  // field with the 'hero_narrow' image style. If not, it will fall back to the
  // 'lg_img' field, also with the 'hero_narrow' image style.
  $hero_sm_url = '';
  if (isset($variables['content']['field_sm_img']['0'])) {
    $hero_sm_url = ImageStyle::load('hero_narrow')->buildUrl($node->field_sm_img->entity->getFileUri());
  else {
    $hero_sm_url = ImageStyle::load('hero_narrow')->buildUrl($node->field_lg_img->entity->getFileUri());
  // We pass both urls to the template
  $variables['hero_lg_url'] = $hero_lg_url;
  $variables['hero_sm_url'] = $hero_sm_url;
  // The 'lg_img' field has a required alt field. The 'sm_img' field doesn't have
  // an alt field, since the <picture> element only takes one alt value.
  $lg_field = $node->get('field_lg_img')->getValue();
  $variables['hero_alt'] = $lg_field[0]['alt'];

Finally, you'll call these variables in your node template, like so:

  <source srcset="{{ hero_lg_url }}" media="(min-width: 600px)" />
  <img src="{{ hero_sm_url }}" alt="{{ hero_alt }}" />

This will cause your larger image to show on viewport widths of at least 600px, and the small image to show on viewport widths below that. Of course, you can take this a step further by tying it in with your theme's already-defined breakpoints, using more than two images, or changing your fallback behavior.

If you've found this helpful, or want to talk about more outside-the-box Drupal image tricks, come say hi at my DrupalCon Nashville session next month, Taking Images to the Next Level in Drupal 8.