Use different image content for mobile and desktop in Drupal 8 (Art direction)

This tutorial shows how to create a reusable container to display different images dependent on the viewport width.


Scenario

The website should display a different image depending on the viewport size. These images are art-directed, which means they will differ in content and layout and are created specifically for desktop and mobile respectivly. 

The goal is to have a reusable concept which allows using our existing image optimization pipelines, but will not include patching of existing components and will minimize site traffic. The solution should also work for Internet Explorer 11 ;(

Serving both images and using CSS to hide one or the other, is out of the question as it will ultimately download both images.

The picture element is a good start but rendering it manually would circumvent our existing image optimizations.


Components

We use the following components:

Drupal 8

Paragraphs Module (Optional)

Responsive Images Module

reSmush.it image style optimizer Module (Optional)

WebP Module (Optional)


Step 1: Create Image Container

We use a paragraph to create a container which holds a field for the mobile image and another one for the desktop image. You can do it without a paragraph and create your own field type though.






A little bonus is the field "Only mobile image" which allows the user to add just one image and to use it for every viewport size.

Step 2: Create Responsive Image Styles for Mobile and Desktop

Responsive Image Styles allow Drupal to serve different variants of an image depending on the viewport size using the picture element. The result (we use WebP aswell) will look like this. You can easily see the media query (min-width:1200px). Please notice the last img element which is used as fallback. 



We will create two different Responsive Image Styles, one only for mobile and the other for desktop. We will define every viewport width which is less than 767px as mobile.

The mobile style is configured to show only an image for a min-with of 0px and another variant for a min-width of 480px. 

The desktop style is configured to show different image variants for min-width above 768px, 991px and so on.

Step 2.1 Prevent fallback image to ruin it

Both styles have a fallback image needed to support IE11 which does not support the picture element at all. This is a problem since we want the desktop image to be empty in mobile and vice versa.

First we use a hook to prevent the fallback image from rendering.

function od_helper_preprocess_responsive_image(&$variables)
{
  $responsive_image_style = $variables['responsive_image_style_id'];

  // Prevent fallback-image from rendering by removing src and alt and store it in data attributes. This fallback-images are later repaired depending on viewport size by js behaviour px1_ResponsiveImageContainerActivateFallbacks
 
  if ($responsive_image_style == "mobile" || 
      $responsive_image_style == "desktop") {

    $img = $variables["img_element"];
    $img["#attributes"]["data-src"] = $img["#uri"];
    $img["#attributes"]["data-alt"] = $img["#alt"];
    $img["#uri"] = "";
    $img["#alt"] = "";
    $variables["img_element"] = $img;
  }
}


Then we use a JS behaviour to activate the correct image depending on viewport width.

  Drupal.behaviors.px1_ResponsiveImageContainerActivateFallbacks = {
    attach(context, settings) {
      const px1 = 
      $(".paragraph-px_1_responsive_image_container", context);
      
      if (!px1.length) {
        return;
      }

      function deactivate(img) {
        img.attr("src", "");
        img.attr("alt", "");
      };

      function activate(img) {
        if (!img.attr("src")) {
          img.attr("src", img.attr("data-src"));
        }
        if (!img.attr("alt")) {
          img.attr("alt", img.attr("data-alt"));
        }
      };

      px1.each(function () {
        const container = $(this);
        const imageDesktop = 
              container.find(".field--name-field-image img");
        const imageMobile = 
              container.find(".field--name-field-image-mobile img");

        // If there is only one image, then it is alway activated 
        if (!imageDesktop.length) {
          return;
        }

        function toggleFallBackImage() {
          if ($(window).width() < screen_sm) {
            activate(imageMobile);
            deactivate(imageDesktop);
          } else {
            activate(imageDesktop);
            deactivate(imageMobile);
          }
        }

        customFunctions.onresize(toggleFallBackImage);
      })
    }
  };

Step 2.2: Create Responsive Image Styles for Mobile with max-width

The second problem is that the responsive image only creates media queries with min-width but 
you cannot set a max-width. For the mobile image to disappear in desktop resolution, we need a max-width as well. So we add the following block to the Hookfrom above "od_helper_preprocess_responsive_image" 

function od_helper_preprocess_responsive_image(&$variables)
{
  $responsive_image_style = $variables['responsive_image_style_id'];

    ....
    
  // Prevent mobile images to be shown for viewport-width > 767px.
  if ($responsive_image_style == "mobile") {
    if (isset($variables['sources'])) {
      foreach ($variables['sources'] as $source) {
        $value = $source["media"]->value();
        $value = $value . " and (max-width:767px)";
        $source["media"] = new AttributeString("media", $value);
      }
    }
  }

Step 3: Configure Image Container

Just use the matching Responsive Image Styles for each image




Bonus - Allow user to use only one image for each viewport size 


As mentioned above, we added a field "Only mobile image" which allows the user to add just one image and to use it for every viewport size. If this is activated, the Responsive Image Style for the mobile image is changed so that this image ist used for every viewport size. The desktop image is removed in this case.

function od_helper_ds_pre_render_alter(&$layout_render_array, $context, &$vars)
{
  if ($bundle == 'px_1_responsive_image_container') {
    
    $use_mobile_image = 
    $paragraph->field_only_mobile_image->getValue()[0]['value'];
    if($use_mobile_image){
      unset($layout_render_array["ds_content"][1]);
      $layout_render_array["ds_content"][0][0]                              ["#responsive_image_style_id"] = "wide"; 
    }
  }

Conclusion

Once the principles are understood, it is a straightforward solution which can be easily adapted to different use cases. The end result should look like this (Sample has viewport-width > 768px so only the fallback img for desktop is activated).



TLDR;

The key is to use 2 different Images (rendered as picture elements) which are positioned at the same place.

One Images uses the Responsive Image Styles "Mobile Only" and the other "Desktop only". So for any given viewport, there is only one image loaded and visible

We can use a php Hook to add a max-width to the Mobile Only Image Style, so its not visible in Desktop mode and to prevent the default img tag from rendering.

function od_helper_preprocess_responsive_image(&$variables)
{
  $responsive_image_style = $variables['responsive_image_style_id'];

  // Prevent mobile images to be shown for viewport-width > 767px.
  if ($responsive_image_style == "mobile") {
    if (isset($variables['sources'])) {
      foreach ($variables['sources'] as $source) {
        $value = $source["media"]->value();
        $value = $value . " and (max-width:767px)";
        $source["media"] = new AttributeString("media", $value);
      }
    }

  // Prevent fallback-image from rendering by removing src and alt and store it in data attributes. This fallback-images are later repaired depending on viewport size by js behaviour px1_ResponsiveImageContainerActivateFallbacks
 
  if ($responsive_image_style == "mobile" || 
      $responsive_image_style == "desktop") {

    $img = $variables["img_element"];
    $img["#attributes"]["data-src"] = $img["#uri"];
    $img["#attributes"]["data-alt"] = $img["#alt"];
    $img["#uri"] = "";
    $img["#alt"] = "";
    $variables["img_element"] = $img;
  }
}

We can later activate the appropriate default img via js depending on viewport size. This is relevant for IE which does not support the picture element.

Kommentare

Beliebte Posts aus diesem Blog

Embed videos GDPR (DSGVO) compliant in Drupal 8