DEV Community

Cover image for Start and stop a llama! How to create a non-autoplay GIF web component πŸŽžοΈπŸ›‘πŸ”₯
Pascal Thormeier
Pascal Thormeier

Posted on

Start and stop a llama! How to create a non-autoplay GIF web component πŸŽžοΈπŸ›‘πŸ”₯

Autoplay can be pesky. Moving things are taking away the users focus. A listicle with lots of auto-play gifs looks waaay to busy - thank goodness gifs don't have sound, right?

Today, I'll show you how to create a web component that allows your users to decide if they want to play a gif or not! Let's get started.

Some very cute test data

I got on A Popular Search Engineβ„’ and looked for "example gif" - the result was underwhelming. I was hoping for some stock gifs to use, but whelp, all I found was this insanely cute interaction of a baby llama and a cat:

A llama and a cat playing

Weee, that's adorable! I could look at this all day. Wait - I can! Lucky me!

Building the web component

So, for this web component, we need a few things:

  • A canvas (where the "thumbnail" will live)
  • An image (the actual gif)
  • A label that says "gif"
  • Some styling

Let's do just that:

const noAutoplayGifTemplate = document.createElement('template')
noAutoplayGifTemplate.innerHTML = `
<style>
.no-autoplay-gif {
  --size: 30px;
  cursor: pointer;
  position: relative;
}

.no-autoplay-gif .gif-label {
  border: 2px solid #000;
  background-color: #fff;
  border-radius: 100%;
  width: var(--size);
  height: var(--size);
  text-align: center;
  font: bold calc(var(--size) * 0.4)/var(--size) sans-serif;
  position: absolute;
  top: calc(50% - var(--size) / 2);
  left: calc(50% - var(--size) / 2);
}

.no-autoplay-gif .hidden {
  display: none;
}
</style>
<div class="no-autoplay-gif">
  <canvas />
  <span class="gif-label" aria-hidden="true">GIF</span>
  <img class="hidden">
</div>`
Enter fullscreen mode Exit fullscreen mode

Next, we'll create a class that derives from HTMLElement. This class will contain the play/stop toggle behaviour later on.

class NoAutoplayGif extends HTMLElement {
  constructor() {
    super()

    // Add setup here
  }

  loadImage() {
    // Add rendering here
  }

  static get observedAttributes() {
    return ['src', 'alt'];
  }

  attributeChangedCallback(name, oldVal, newVal) {
    if (oldVal !== newVal || oldVal === null) {
      this.loadImage()
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

There's also a bit of boilerplating in here: An empty render function that will load the image and display the thumbnail, as well as a constructor and some web component specific methods.

Ok, that's a lot of code already. Let me explain.

The loadImage function isn't called automatically, we need to do that ourselves. The function attributeChangedCallback lets us define what happens when any of the specified attributes of observedAttributes changes. In this case: Load the image and display it. What the browser roughly does is this:

  • Encounter web component
  • Call its constructor (calls constructor())
  • Set its attributes one by one as set in the DOM (so, src="llama.gif" calls .setAttribute('src', 'llama.gif')
  • Execute attributeChangedCallback for every changed attribute

When checking in the constructor, those attributes will be empty at first and only filled later on. If we need one or more attributes to actually do some rendering, there's no point in calling the loadImage function if we know those attributes aren't there. So we don't call it in the constructor, but only when there's a chance of the attribute being around.

To finish up the boilerplating, let's define this class as our custom web component:

class NoAutoplayGif extends HTMLElement {
  // ...
}

window.customElements.define('no-autoplay-gif', NoAutoplayGif)
Enter fullscreen mode Exit fullscreen mode

We can now use this component like so:

<no-autoplay-gif 
  src="..." 
  alt="Llama and cat" 
/>
Enter fullscreen mode Exit fullscreen mode

Off for a good start!

The logic

Now comes the fun part. We need to add the noAutoplayGifTemplate as the components shadow DOM. This will already render DOM, but we still cannot do much without the src and the alt attribute. We therefore only collect some elements from the shadow DOM we'll need later on and already attach a click listener to toggle the start/stop mode.

class NoAutoplayGif extends HTMLElement {
  constructor() {
    super()

    // Attach the shadow DOM
    this._shadowRoot = this.attachShadow({ mode: 'open' })

    // Add the template from above
    this._shadowRoot.appendChild(
      noAutoplayGifTemplate.content.cloneNode(true)
    )

    // We'll need these later on.
    this.canvas = this._shadowRoot.querySelector('canvas')
    this.img = this._shadowRoot.querySelector('img')
    this.label = this._shadowRoot.querySelector('.gif-label')
    this.container = this._shadowRoot.querySelector('.no-autoplay-gif')

    // Make the entire thing clickable
    this._shadowRoot.querySelector('.no-autoplay-gif').addEventListener('click', () => {
      this.toggleImage()
    })
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

To not run into undefined method errors, we add these three methods as well:

class NoAutoplayGif extends HTMLElement {
  // ...
  toggleImage(force = undefined) {
    this.img.classList.toggle('hidden', force)

    // We need to check for undefined values, as JS does a distinction here.
    // We cannot simply negate a given force value (i.e. hiding one thing and unhiding another)
    // as an undefined value would actually toggle the img, but
    // always hide the other two, because !undefined == true
    this.canvas.classList.toggle('hidden', force !== undefined ? !force : undefined)
    this.label.classList.toggle('hidden', force !== undefined ? !force : undefined)
  }

  start() {
    this.toggleImage(false)
  }

  stop() {
    this.toggleImage(true)
  }
  // ...
}
Enter fullscreen mode Exit fullscreen mode

The start/stop methods allow us to force-start or force-stop the gif. We could, in theory, now do something like this:

const gif = document.querySelector('no-autoplay-gif')
gif.start()
gif.stop()
gif.toggleImage()
Enter fullscreen mode Exit fullscreen mode

Neat!

Finally, we can add the image loading part. Let's do some validation first:

class NoAutoplayGif extends HTMLElement {
  // ...
  loadImage() {
    const src = this.getAttribute('src')
    const alt = this.getAttribute('alt')

    if (!src) {
      console.warn('A source gif must be given')
      return
    }

    if (!src.endsWith('.gif')) {
      console.warn('Provided src is not a .gif')
      return
    }

    // More stuff
  }
  // ...
}
Enter fullscreen mode Exit fullscreen mode

And as a last step, we can load the image, set some width and height and put the canvas to use:

class NoAutoplayGif extends HTMLElement {
  // ...
  loadImage() {
    // Validation

    this.img.onload = event => {
      const width = event.currentTarget.width
      const height = event.currentTarget.height

      // Set width and height of the entire thing
      this.canvas.setAttribute('width', width)
      this.canvas.setAttribute('height', height)
      this.container.setAttribute('style', `
        width: ${width}px;
        height: ${height}px;
      `)

      // "Draws" the gif onto a canvas, i.e. the first
      // frame, making it look like a thumbnail.
      this.canvas.getContext('2d').drawImage(this.img, 0, 0)
    }

    // Trigger the loading
    this.img.src = src
    this.img.alt = alt
  }
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Aaand we're done!

The result

Nice!


I hope you enjoyed reading this article as much as I enjoyed writing it! If so, leave a ❀️ or a πŸ¦„! I write tech articles in my free time and like to drink coffee every once in a while.

If you want to support my efforts, you can offer me a coffee β˜• or follow me on Twitter 🐦! You can also support me directly via Paypal!

Buy me a coffee button

Top comments (7)

Collapse
 
joelbonetr profile image
JoelBonetR πŸ₯‡

At least to me (using Chrome in Android) it's more like a play/restart isn't it? Was this intended or a bug?

Collapse
 
thormeier profile image
Pascal Thormeier • Edited

Kind of intended. It's generally really hard to extract single frames from gifs and to figure out at which frame you actually need to pause. The return on investment is miniscule if you need to add several libs and hundreds of lines of code for the pausing to be smooth. The easy way out is to "restart" the gif by replacing it with the first frame again and canvas allows for just that.

Or am I understanding you wrong? Do you perhaps mean that the stopping doesn't work and it's always plaing on only restarting on click?

Collapse
 
joelbonetr profile image
JoelBonetR πŸ₯‡

You understood it correctly. I was thinking off it because I don't usually work with GIFs but I remembered a pair of concepts from a post.
Now I double-checked it and probably .gifv format can help you to reach that easier.

Context and information:

GIF is a popular format for sharing short animation loops on the web, which includes sites such as 9gag, Imgur and Twitter.
The GIFV format is part of Imgur's "Project GIFV" initiative to improve the GIF format. One of the main upgrades of the format is that GIFV files are significantly smaller than GIF files, which enables faster loading speeds.

A GIFV file is a video file saved in a format developed by Imgur that improves upon the GIF format. It contains video compressed in the H.264 format and stored inside an .MP4 file container.

It lets you right-click to "show controls" and I bet there should be a way to programatically make it the default.
You can see gifv in use inside imgur (obviously) and in 9gag as well :)

Thread Thread
 
thormeier profile image
Pascal Thormeier

Very good point! I haven't worked with GIFV before, but I'm glad to see that things like these get institutionalized in modern browsers. I've actually found a Firefox ticket that wants to add video controls for all animated images, but that thing's open since 12 years: bugzilla.mozilla.org/show_bug.cgi?... - perhaps this will come one day, just like the controls for the HTML video tag. I'd really love to be able to add controls and steer it with some attributes, that would be amazing!

Thread Thread
 
joelbonetr profile image
JoelBonetR πŸ₯‡

Well, some of those things require a more in deep refactor and probably, the format itself can't expose this behaviour for the browser to implement/use. In fact, gifv adds those capabilities to animated gifs so I assume that it's the way to go.

If you've a site where you allow users to upload animated gifs, you can convert gif to gifv format on the fly and store this last one, so you standarize the output while allowing both formats as input.

This has been done in the past and in the present as well for different formats.
E.g: I can remember converting xls and xlsx to csv for convenience on working with CSV only internally inside the webapp but also having the opposite of converting this internal csv to xlsx just for user to download.
Same on allowing microsoft word, open document and so on but storing that as rich text and parsing the output into PDF.

Collapse
 
yjdoc2 profile image
YJDoc2 • Edited

Hey really cool article, I wanted to read about custom HTML components for some time, and this is great !!!
Thanks for such nice explanation and walkthrough πŸ˜ƒπŸ˜„

Edit : forgot to mention that this is such a great idea, to have play and pause for gifs. I have seen some pages where after loading the gifs would distract front the core content. I feel this should also be a great help for flashing/trippy text, where rather than warning one can give control to user what and when to run. Such a great idea !

PS : forgive me for saying this, but shouldn't the 'cure' in first header in the article be 'cute'?

Collapse
 
thormeier profile image
Pascal Thormeier

Thank you so much, so glad you liked it! I was browsing some news website and saw the pausable gifs and thought "I could probably do that!" - and here we are. :) It's really all about observing and trying to rebuild, I also learnt a lot when writing this article. Features like these open up new possibilities for accessibility. Safari doesn't auto-play videos with sound, for example, that's a good start, but I've seen muted auto-playing videos as hero images that really shouldn't be auto-played...

Thank you for spotting the typo, fixed it :D