Animated list with sliding background
Tags: vue, javascript, tailwindcss, animation
Published: 2021-02-16 08:35:50

On Twitter recently, I announced the creation of my first ever codepen! 🎉
{% codepen https://codepen.io/katie-adams/pen/NWbRaPg %}

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:
{% codepen https://codepen.io/katie-adams/pen/oNYYOwd %}

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](https://pokeapi.co/). 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"
      />
```
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";
```
This line brings in some of the Composition API 'stuff' that is available in Vue 3. I recommend reading [Dan Vega's post](https://www.danvega.dev/blog/2020/02/12/vue3-ref-vs-reactive/) on the Ref and there's also some good [documentation](https://v3.vuejs.org/guide/computed.html#computed-caching-vs-methods) 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";
      }
    }
```
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)"
```
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
          )
        );
      });
```
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!