Drooling cat

July 27, 2022 4 min read

This is a very simple drooling cat animation created with React, Emotion, and Figma. As the page scrolls down, the saliva gets fixed at the top of the page, giving the impression that the cat is drooling.

Designing in Figma#

Let's start by designing a cat that is drooling in Figma and then split the image in two: "bored-cat" and "saliva".

drooling cat figma

A hover animation would also be nice for the bored cat. Hovering the image will make the cat bored. As you can see in the image below, I am going to group the eyelids into two different groups. One I call "evil-lids" and the other I call "bored-lids". I'm going to leave both groups visible.

drooling cat figma

We could export the images in two files, but since we want to have some hover animations, let's export them as SVGs as React components. In order to accomplish this, I will use the Figma plugin SVG to JSX.

Coding in React#

Using React, I will make a component called DroolingCat. And I'm going to add both SVGs. The first thing I'm going to do is position them. The saliva is going to be absolutely positioned on top of the bored cat. Exactly at the bottom of the mouth.

import { useState, useEffect, useRef } from "react";
import { css } from "@emotion/react";

function DroolingCat() {
  return (
    <div
      css={css`
        position: relative;
      `}
    >
      // Saliva
      <svg
        css={css`
          position: absolute;
          top: 50%;
          margin-top: 17%;
          width: 18px;
          left: 50%;
          margin-left: -9px;
        `}
      />
      // Bored cat
      <svg />
    </div>
  );
}

export default DroolingCat;

The drooling cat effect#

Having positioned the saliva, it's now time to create the first animation. The goal is to give the impression that the cat is drooling.

Firstly, I'm going to use the useRef hook to keep track of the position of the cat's mouth. Then I'm going to create a handleScroll function that will be called when the page is scrolled. Inside this function we need to detect if the cat's mouth is in the viewport. If it is, we will setSticky to false. If it is not, we will setSticky to true.

The saliva is supposed to become fixed to the top of the screen when the cat's mouth is out of viewport. As a result, we're going to update the styles based on the isSticky state.

Why am I not using the CSS position sticky if I'm calling the state isSticky? In case the parent element scrolls off the viewport, the element with position: sticky will leave the viewport as well and we don't want that. We want the saliva to stay fixed to the top of the viewport.

import { useState, useEffect, useRef } from "react";
import { css } from "@emotion/react";

function DroolingCat() {
  const [isSticky, setSticky] = useState(false);
  const mouthRef = useRef(null);

  useEffect(() => {
    window.addEventListener("scroll", handleScroll);
    return () => {
      window.removeEventListener("scroll", () => handleScroll);
    };
  }, []);

  const handleScroll = () => {
    if (
      mouthRef &&
      mouthRef.current &&
      mouthRef.current.getBoundingClientRect()
    ) {
      if (mouthRef.current.getBoundingClientRect().bottom <= 0) {
        setSticky(true);
      } else {
        setSticky(false);
      }
    }
  };

  return (
    <div>
      // Saliva
      <svg
        xmlns="http://www.w3.org/2000/svg"
        fill="none"
        viewBox="0 0 30 603"
        css={css`
          ${isSticky
            ? `position: fixed; top: 0;`
            : `position: absolute; top: 50%;  margin-top: 17%;`}
          width: 18px;
          left: 50%;
          margin-left: -9px;
        `}
      >
        // the mouth gets the ref from the mouthRef variable
        <path ref={mouthRef}></path>
      </svg>
      // Bored cat
      <svg />
    </div>
  );
}

export default DroolingCat;

The hover animation#

It is now time to add the hover animation. Whenever the mouse enters or leaves the image, the cat's eyelids should change. When the mouse is hovering the cat, we set the isBored state to true. When the mouse leaves the cat, we set the isBored state to false. The isBored state will determine which eyelids will be displayed inside the SVG. The "evil" eyelids will be displayed if the cat isBored. The "bored" eyelids will be displayed if the cat !isBored (it's not bored).

import { useState, useEffect, useRef } from "react";
import { css } from "@emotion/react";

function DroolingCat() {
  const [isBored, setIsBored] = useState(true);

  return (
    <div>
      // Bored cat
      <svg
        onMouseEnter={() => setIsBored(false)}
        onMouseLeave={() => setIsBored(true)}
      >
        {!isBored && (
          <>
            <path />
            <path />
          </>
        )}

        {isBored && <path />}
      </svg>
    </div>
  );
}

export default DroolingCat;

Putting it all together#

In the following CodeSandbox, you can see how to put all the code together. All the animations can be seen in action by scrolling the page and hovering over the cat. Feel free to recreate this code with your design, and let me know by tweeting @miuki_miu! It would be great to see how you use it. That's all!

Share on TwitterEdit on GitHub

© 2022-present Elizabet Oliveira