CSS-only slideshows in Drupal with keyframes generated in Sass loops

April 25, 2016

One of the principles I’m trying to follow as I rebuild The Gallery Guide is to minimise the amount of code. I’m keen to reduce the number of Drupal modules, but also the amount of JavaScript used, mainly for performance reasons, but also as part of a strategy of progressive enhancement around the site.

On the current site, there are quite a few image slideshows, which are built using the views_slideshow module. Given that I don’t need to configure timings via the UI, I could just handle the slideshows in theme code, maybe using a plugin like Cycle, or perhaps I might not need jQuery. But then I started thinking maybe I can avoid using JavaScript altogether for this.

I found a nice CSS-only slideshow by Dudley Storey, which uses CSS animations. It’s set up for 4 images, with the first image duplicated at the end. Looking at the keyframes code, 25% of the time is allotted to each slide, of which 5% is the transition. On The Gallery Guide, gallery, exhibition, and artist pages can all have an arbitrary number of images attached, so I needed to do a tiny bit of maths to convert that into a more flexible formula. It works nicely in modern browsers without adding much extra code, and falls back to a single image in browsers that don’t support CSS animations.

Seems like an ideal use case for Sass loops - here’s the Sass partial that generates the keyframes code:

/// Set up image carousels. 
///
/// @param {int} $items - the number of items
/// @param {int} $speed [6] - the duration of each item in seconds
///

@mixin carousel($items, $speed: 6) {
  $duration: $items * $speed;
    
  animation: #{$duration}s slidy-#{$items} infinite;  
  width: (100% * $items);
  
  .field__item {
    width: (100% / $items);
    height: auto;
    display: inline-block;
    position: inherit;
  }
}

@for $i from 2 through 5 {
  .carousel-#{$i} {
    @include carousel($i);
  }

  @keyframes slidy-#{$i} {
    /**
     * There are $i images in the carousel.
     * The first image is repeated, so there are $i - 1 distinct images to share the transition time.
     */
    $duration_per_slide: 100 / ($i - 1);
    $moving_time: 5;
    $still_time: $duration_per_slide - $moving_time;

    @for $j from 0 through $i {

      $slide_start: round($j * $duration_per_slide);
      $slide_end: round($slide_start + $still_time);

      // Move everything one image width left for each slide.
      $left_offset: 0 - $j;

      @if ($slide_start <= 100) {
        #{$slide_start}% {
          left: percentage($left_offset);
        }
      }

      @if ($slide_end < 100) {
        #{$slide_end}% {
          left: percentage($left_offset);
        }
      }
    }
  }
}

This is a nice example of using variables in selectors, using the interpolation syntax - for instance, this Sass selector inside the loop .carousel-#{$i} generates the selectors .carousel-2, .carousel-3 and so on. You need to be careful not to end up with bloated output CSS, but I appreciate the ability to avoid repetition and keep the Sass code elegant.

As Sass code becomes more like “real programming” with mixins and functions, there’s more need for formal documentation of code - this is my first time using SassDoc, and I like it so far.

Drupal makes it easy to set up classes with the number of elements, and with Twig in Drupal 8, it isn’t even necessary to write any PHP code to achieve that - it’s simple to create a template for a node type or field, and use the Twig filters to get at any relevant data. In this case, my field is called field_images, so the template is field--field-images.html.twig - there’s more info about template naming conventions on drupal.org, and here’s an extract from the field template:

{% if items|length > 1 %}
  <div class="image-carousel carousel-{{ items|length + 1 }}">
  {% for item in items %}
    <div{{ item.attributes.addClass('field__item') }}>{{ item.content }}</div>
  {% endfor %}

  {# duplicate the first item #}
  <div{{ items[0].attributes.addClass('field__item') }}>{{ items[0].content }}</div>
</div>
{% else %}
  {% for item in items %}
    <div{{ item.attributes.addClass('field__item') }}>{{ item.content }}</div>
  {% endfor %}
{% endif %}

Having been bogged down for a while with my attempts to migrate the site to Drupal 8, it’s a refreshing change to be using new tools and have nothing but good things to say about them - in my experience so far I think that theming Drupal 8 will generally feel less hacky and more enjoyable than 6 and 7 did - I’m looking forward to working on bigger Drupal 8 projects.