DEV Community

loading...
Cover image for Animated list with sliding background

Animated list with sliding background

Katie Adams
Web developer at Greggs, UK with a proficiency in VueJS, Tailwind, and Storyblok, as well as other frameworks. I'm also passionate about web design, and mobile app development.
・4 min read

On Twitter recently, I announced the creation of my first ever codepen! πŸŽ‰

The point of the codepen was to solve a problem I encountered in a Vue project, wherein I'd been tasked with creating an animation I'd never done before. The brief was a pill-like shape that was animated to slide between items when a new one is selected (see the above codepen). CSS animation is not my strong suit; simultaneously learning Vue 3 and the composition API made an already new thing seem even more daunting.

Hence the codepen. My intention was to strip the process back and attempt to achieve the intended result for with good ol' plain JavaScript. And it seemed to work!

So how did it look once I'd translated it back into Vue? Well, it looked like this:

Let's dive a little further in and see what's happening.

The HTML is relatively straightforward to any Vue veteran. We've got an unordered list with a series of list items. Each list item is populated with the name of a Pokemon (lovingly taken from the PokeAPI. There's a couple of wrapper div tags, predominantly for styling but one of them houses the ul and a span that will act as our coloured pill element.

<span
        id="categoryBackground"
        role="presentation"
        class="transition-all duration-300 ease-in-out z-0 absolute rounded-full bg-red-700"
      />
Enter fullscreen mode Exit fullscreen mode

This funky little dude is going to zoom around behind the various list items, happily animated and colourful. Note the role attribute too, letting screen readers know that this is just for show.

Styling is done in Tailwind so I won't delve into that any more than is necessary.

So: the meaty stuff. The nitty gritty. The Javascript. Tasty stuff.

import { ref, computed } from "vue";
Enter fullscreen mode Exit fullscreen mode

This line brings in some of the Composition API 'stuff' that is available in Vue 3. I recommend reading Dan Vega's post on the Ref and there's also some good documentation on Computed Refs too. Long story short, they're reactive. So if you find yourself using data from the VueX store where the content might change frequently, then your data should reflect it when we use these variable types.

The beauty of the computed variable type is that it is reactive (just like the ref) but it also keeps an eye on the data that it depends on. So when that dependent data changes, it updates itself! Pretty cool, right?

In our setup() function, we define a few reactive variables:

  • An array of categories, filled with Pokemon names
  • selectedCategoryName, a self-explanatory string
  • selectedCategoryElement
  • categoryBackground, which just returns our little decorative span element from the DOM
  • selectedCategoryElement will also return a DOM element but it does so using the selectedCategoryName to make sure it's picking up the element with the matching id.

We'll come back to the selectedCategoryElement variable. It uses a function that is worth going over first:

function updateCategoryBackground(category) {
      selectedCategoryElement = document.querySelector(
        `#category${category.name}`
      );
      if (selectedCategoryElement && categoryBackground.value) {
        categoryBackground.value.style.width =
          selectedCategoryElement.scrollWidth + "px";
        categoryBackground.value.style.height =
          selectedCategoryElement.scrollHeight + "px";
        categoryBackground.value.style.left = selectedCategoryElement.offsetLeft + "px";
      }
    }
Enter fullscreen mode Exit fullscreen mode

This is our updateCategoryBackground() function. This bad boy works the magic we're looking for with this animation. Firstly, it updates our selectedCategoryElement variable with the DOM element of the clicked category. Then, provided that this new element actually exists and that our decorative span was successfully found too, it updates the stylings of the latter to match the former! So if the Bulbasaur button is clicked, then our pill-shaped doodad will be told what size the button is and where it is, and he'll rush to copy.

Thanks to the Tailwind classes on the decorative span, any transformations that occur on it - such as changes in size or position - are animated in an ease-in and ease-out fashion. Stupidly simple stuff but possibly not for someone who's never done it before.

So when does the updateCategoryBackground() function even get called? Well, we've got another function called selectedCategoryChanged(). Take another look at the unordered list in our template:

@click="selectedCategoryChanged(category)"
Enter fullscreen mode Exit fullscreen mode

Each list item has an click event handler that uses - you guessed it - the selectedCategoryChanged() function. This function updates the name of the selected value, thus updating the computed functions that rely on it. Then it calls the updateCategoryBackground() function to move our funky little pill around the screen!

I purposefully left the selectedCategoryElement variable until last because it does a couple of different things.

Vue.nextTick(() => {
        updateCategoryBackground(
          categories.value.find(
            (cat) => cat.name === selectedCategoryName.value
          )
        );
      });
Enter fullscreen mode Exit fullscreen mode

As you can see, it calls the updateCategoryBackground() function but is encapsulated in this Vue.nextTick() arrow function. The nextTick() function pushes back when the code runs. It waits until the DOM has rendered. This is important because the updateCategoryBackground function updates the style attribute of our decorative span. It's important that we know it will even be there to receive our update, otherwise we'll get a whole range of error messages.

Lastly, it returns the selected category from the DOM to ensure we have a default value when the app is first loaded. In this case, "Bulbasaur".

And that's it!

There's obviously a lot of ways this can be expanded to include different features and include different stylings. For instance, you can quite easily switch this up to include the usage of the Vuex store!

Let me know if you use this elsewhere or have a play yourself. It'd be great to see what improvements or changes get made!

Discussion (0)