Converting animated button to React

I was looking at a nice little animation of a button on https://codepen.io/andrewmillen/pen/MoKLob and thought it’d be interesting turning it into a React component especially as I’m currently playing with animejs. As usual such things never “just work” but that also means it’s a good learning process.

So here goes, first off create your React application and add the animejs library…

  • yarn create react-app animatedbutton –typescript

From within the generated application run the following

  • yarn add animejs
  • yarn add @types/animejs

Note: I will not list all the code here, it’s available on GitHub, but I’ll list the changes and things of interest.

Next create a components folder off of the src folder and create AniButton.css, copying the CSS from the original link into that and then create AniButton.jsx which I’ve included here

Note: I used a jsx file as opposed to tsx simply because I didn’t want to spend too long fixing “type” issues initially, but when I did try this I found the ref is not expected on a path element, so we’ll stick with JSX for now.

export class AniButton extends React.Component {

  checkRef = React.createRef();

  constructor(props) {
    super(props);

    this.handleOnClick = this.handleOnClick.bind(this);
  }

  handleOnClick() {

    var pathEls = this.checkRef.current;
    var offset = anime.setDashoffset(pathEls);
    pathEls.setAttribute("stroke-dashoffset", offset);

    var basicTimeline = anime.timeline({
      autoplay: false
    })
    .add({
      targets: ".text",
      duration: 1,
      opacity: 0
    })
    .add({
      targets: ".button",
      duration: 1300,
      height: 10,
      width: 300,
      backgroundColor: "#2B2D2F",
      border: "0",
      borderRadius: 100
    })
    .add({
      targets: ".progress",
      duration: 2000,
      width: 300,
      easing: "linear"
    })
    .add({
      targets: ".button",
      width: 0,
      duration: 1
    })
    .add({
      targets: ".progress",
      width: 80,
      height: 80,
      delay: 500,
      duration: 750,
      borderRadius: 80,
      backgroundColor: "#71DFBE"
    })
    .add({
      targets: pathEls,
      strokeDashoffset: [offset, 0],
      opacity: 1,
      duration: 200,
      easing: "easeInOutSine"
    });

    basicTimeline.play();
  }

  render() {
    return (
      <main>
        <div className="button" onClick={this.handleOnClick}>
          <div className="text" onClick={this.handleOnClick}>Submit</div>
        </div>
        <div className="progress"></div>
        <svg x="0px" y="0px"
          viewBox="0 0 25 30">
          <path ref={this.checkRef} className="check st0" d="M2,19.2C5.9,23.6,9.4,28,9.4,28L23,2" />
        </svg>
      </main>
    )
  }
}

Note: A couple of changes I made to the CSS was change the progress bar name and fixed the background to gray. The reason I did this was simply because I had problems initially with the CSS displaying incorrectly and messed with things to try to resolve – I think the problem is unrelated to the changes I made though.

Also I seem to periodically lose the progress bar, not 100% why but just saying.

The key differences to the original source code are – obviously it’s coded into a React.Component. Event handlers are within the class, but really the main difference is the way we get access to the DOM path element via the ref={this.checkRef}. I’ve removed the loop from the original code which found the paths as we only have one here.

Next, the original code uses the Poppins font, so add the following the public/index.html

<link 
   href="https://fonts.googleapis.com/css?family=Poppins:600" 
   rel="stylesheet">    

The animejs code

At this point everything works nicely, so let’s look at the animejs code.

We need to get a reference to the DOM path object using

var pathEls = this.checkRef.current;
var offset = anime.setDashoffset(pathEls);
pathEls.setAttribute("stroke-dashoffset", offset);

The anime.setDashoffset code does the following

“Creates path drawing animation using the ‘stroke-dashoffset’ property. Set the path ‘dash-offset’ value with anime.setDashoffset() in a from to formatted value.”

Next we create an animejs, which basically allows us to coordinate each stage/step of the animation. Hence the first timeline is to immediately hides the .text using the opacity property and a very short duration.

Now we change the shape of the .button to a thinner button etc.

The next step is to display a progress overlay which starts from the centre (based upon the CSS) and expands out over it’s duration in a linear easing.

Once the previous step’s completed which reduce the .button to zero width to basically hide it and the .progress then changes colour and eventually turns into a circle.

Finally the check/tick is displayed using the strokeDashOffset, so basically this animates the drawing of the check.

CSS

For the most part the animejs timeline code is pretty simple and works a treat, so let’s take a look at the CSS side of things.

Firstly we can import the CSS using

import "./AniButton.css";

Which basically tells webpack (in a standard React application) to include the CSS in the bundle. Now the CSS can be applied to our elements in the usual ways, on an element type or via id or class (in React class becomes classname).

As stated previously though, if you require actual access into the DOM you’ll need to use something like a ref, but use sparingly.

CSS files like the ones we’re using are not really the preferred way to use CSS in React (see the React: CSS in your JS by Christopher Chedeau for some reasons why this is the case, admittedly it’s a few years old but still relevant now).

To save you watching the video, CSS files defines CSS globally which can make styling difficult to debug or understand at times. Also it’s equally difficult to know when CSS is redundant, i.e. an element s no longer used but we still maintain it’s CSS etc.

I’m not going to go into all the others ways to use CSS it this post, but obviously our best way to use CSS would be to have it used locally inlined with our component.

Hence we can instead convert our CSS to the following code within our AniButton.jsx file like this

const styles = {
  main: {
    height: "100vh",
    width: "100vw"
  },
  button: {
    background: "#2B2D2F",
    height: "80px",
    width: "200px",
    textAlign: "center",
    position: "absolute",
    top: "50%",
    transform: "translateY(-50%)",
    left: "0",
    right: "0",
    margin: "0 auto",
    cursor: "pointer",
    borderRadius: "4px"
  },
  text: {
    font: "bold 1.25rem/1 poppins",
    color: "#71DFBE",
    position: "absolute",
    top: "50%",
    transform: "translateY(-52%)",
    left: "0",
    right: "0"
  },
  progress: {
    position: "absolute",
    height: "10px",
    width: "0",
    right: "0",
    top: "50%",
    left: "50%",
    borderRadius: "200px",
    transform: "translateY(-50%) translateX(-50%)",
    background: "gray",
  },
  svg: {
    width: "30px",
    position: "absolute",
    top: "50%",
    transform: "translateY(-50%) translateX(-50%)",
    left: "50%",
    right: "0",
    enableBackground: "new 0 0 25 30"
  },
  check: {
    fill: "none",
    stroke: "#FFFFFF",
    strokeWidth: "3",
    strokeLinecap: "round",
    strokeLinejoin: "round",
    opacity: "0"
  }   
}

Note: underline or hyphens turn into camelCase names and values get wrapped in quotes.

Now we will need to explicitly add the styles to our render code using style={} syntax, so it looks like this

<main style={styles.main}>
  <div className="button" style={styles.button} 
       onClick={this.handleOnClick}>
     <div className="text" style={styles.text} 
       onClick={this.handleOnClick}>Submit</div>
  </div>
  <div className="progress" style={styles.progress}></div>
  <svg x="0px" y="0px"
    viewBox="0 0 25 30" style={styles.svg}>
    <path ref={this.checkRef} style={styles.check} 
      className="check st0" d="M2,19.2C5.9,23.6,9.4,28,9.4,28L23,2" />
  </svg>
</main>

We can now delete our AniButton.css file.