Miguel.nz

Toggle Menu with the Animation API (Javascript)

January 13, 2020   |   3 minutes read.

The Animation API is an experimental Web API that provides animation controls, keyframes and a timeline to a DOM element or source. Similar properties from CSS are now available on Javascript. On this article you will see how to animate a mobile menu using only vanilla JS. We will first see two important constructor classes: Animation and KeyframeEffect.

Animation

Represents a single animation player. It accepts two parameters: effect and timeline. In this article I use effect only. effect is a KeyframeEffect object instance.

Keyframe Effect

Used on the Animation object. It creates a keyframe effect. It accepts three parameters: the DOM Element, keyframe set and keyframe Options.

The HTML

Let’s say we have a menu trigger:

<div class="item--trigger"><a href="#">Menu</a></div>

And we also have a Menu that will open and close:

<div class="item item--menu">
  <nav>
    <ul>
        <li><a href="#">Home</a></li>
        <li><a href="#">Case Studies</a></li>
        <li><a href="#">Contact</a></li>
    </ul>
  </nav>
</div>

The header then should looks like this:

<header>
  <div class="item--logo-wrapper">
    <div class="item item--logo">My website</div>
    <div class="item--trigger"><a href="#">Menu</a></div>
  </div>
  <div class="item item--menu">
    <nav>
      <ul>
          <li><a href="#">Home</a></li>
          <li><a href="#">Case Studies</a></li>
          <li><a href="#">Contact</a></li>
      </ul>
    </nav>
  </div>
</header>

Javascript

I will start with a headerMenu class with a constructor which includes the elements to handle, the event listener and the Animation object and keyframeEffect object.

class headerMenu { 
  constructor() {

    // The trigger and menu wrapper
    this.triggerMenu = document.querySelector('.item--trigger a');
    this.menuWrapper = document.querySelector('.item--menu');

    // The event Listener
    this.toggleMenu = this.toggleMenu.bind(this);
    this.triggerMenu.addEventListener('click', this.toggleMenu);

    // The animation
    this.openMenuAnimation = new Animation(this.menuKeyFrames({
        duration: 200, 
        fill: 'forwards' 
      }));

    this.closeMenuAnimation = new Animation(this.menuKeyFrames({
        duration: 200, 
        fill: 'forwards',
        direction: 'reverse'
      }));
  }
}

The Keyframes
openMenuAnimation and closeMenuAnimation are both using a method called menuKeyFrames defined under headerMenu. Both of them share the same animation effect, however they do have different keyframe options, note the direction: reverse; on this.closeMenuAnimation.
menuKeyFrames looks like this:

menuKeyFrames(options) {
  const menuNavHeight = parseInt(this.menuWrapper.querySelector('nav').getBoundingClientRect().height);
  return new KeyframeEffect( 
    this.menuWrapper,
    [{ 
        height: 0,
        opacity: 0,
      },
      { 
        height: `${menuNavHeight}px`,
        opacity: 1
      }], 
    options 
  );
}

We are basically animating opacity from 0 to 1 and height from 0 to its direct child element’s height dinamically. Note that we are doing this without using jQuery or any other hack and trick. This means, if the menu element changes, this will keep working. This is a game changer for its simplicity and power, no more external libraries, just plain vanilla Javascript.

The event listener
To get this work we will play or cancel our animation methods accordingly:

toggleMenu(event) {
  event.preventDefault();
  if (!this.triggerMenu.classList.contains('active')) {
    this.triggerMenu.classList.add('active');
    this.closeMenuAnimation.cancel();
    this.openMenuAnimation.play();
  } else {
    this.triggerMenu.classList.remove('active');
    this.openMenuAnimation.cancel();
    this.closeMenuAnimation.play();
  }
}

Wrapping up
Our headerMenu class then should look like this:

class headerMenu { 
  constructor() {
    // The trigger and menu wrapper
    this.triggerMenu = document.querySelector('.item--trigger a');
    this.menuWrapper = document.querySelector('.item--menu');

    // The event Listener
    this.toggleMenu = this.toggleMenu.bind(this);
    this.triggerMenu.addEventListener('click', this.toggleMenu);

    // The animation
    this.openMenuAnimation = new Animation(this.menuKeyFrames({
      duration: 200, 
      fill: 'forwards' 
    }));

    this.closeMenuAnimation = new Animation(this.menuKeyFrames({
      duration: 200, 
      fill: 'forwards',
      direction: 'reverse'
    }));
  }

  menuKeyFrames(options) {
    const menuNavHeight = parseInt(this.menuWrapper.querySelector('nav').getBoundingClientRect().height);
    
    return new KeyframeEffect( 
      this.menuWrapper,
      [{ 
          height: 0,
          opacity: 0,
        },
        { 
          height: `${menuNavHeight}px`,
          opacity: 1
        }], 
      options 
    );
  }

  toggleMenu(event) {
    event.preventDefault();
    if (!this.triggerMenu.classList.contains('active')) {
      this.triggerMenu.classList.add('active');
      this.closeMenuAnimation.cancel();
      this.openMenuAnimation.play();
    } else {
      this.triggerMenu.classList.remove('active');
      this.openMenuAnimation.cancel();
      this.closeMenuAnimation.play();
    }
  }
}

You can check all together on my codepen here:
https://codepen.io/miguel/pen/vYEraVj