React, MUI, and Keyframes

One day at work I needed to animate a button so that it shook horizontally when the user clicked it if it was disabled. We rarely do animations, so we generally implement these animations in CSS rather than using a library to accomplish it. That said, we do use React and MUI. However, MUI doesn't have a shake animation, so, I had to implement it myself.

To do so, I used react state, keyframes, and the sx prop in MUI. Let's look at an example starting place.

type MyButtonProps = {
    disabled?: boolean;
};

const sxStyles = {
    button: {
        marginTop: 1,
        padding: 1,
    }
};

export const MyButton = ({ disabled }: MyButtonProps) => {
    const handleClick = () => console.log("You did it!");

    return (
        <Button
            disabled={disabled}
            onClick={handleClick}
            sx={sxStyles.button}>
            Click it!
        </Button>
    );
};

Here we have a simple button that we can disable and it logs to the console when you click it. Nothing fancy. Note that we're using the sx prop on the MUI button.

Now let's make it interesting. Let's add some code so that the button shakes when the user clicks it if it's disabled. First, we'll define the keyframes for our animation.

const animateShake = keyframes`
    0%, 100% {
        translateX(0);
    }
    20%, 60% {
        translateX(-5px);
    }
    40%, 80% {
        translateX(5px);
    }
`;

These keyframes define how the element moves at different stages of the animation. We're telling it to start and end exactly where it is. We then say it should start moving. When it's 20% thought the animation, it should have moved to the left 5 pixels. When it's 40% through the animation, it should have moved to the right 5 pixels from its original position. Then again to the left for 60% and again to the right for 80% until it comes back to its starting position when the animation is 100% completed.

By defining these keyframes, the browser will move the element left and right as we coded it so that it appears as though the element using this animation is shaking left and right.

Next, we'll make a class that utilizes that animation. Defining the animation is only half of it. Once defined, we then need to utilize it in a CSS class so the browser knows when to apply the animation. Note the shake class below.

const sxStyles = {
    button: {
        marginTop: 1,
        padding: 1,
    },
    shake: {
        animation: `${animateShake} 0.5s infinite alternate`,
    },
};

Here we are defining a class and telling CSS to use an animation. The animation it uses is the keyframes we defined above to shake left and right. We then provide some instructions saying it should take 0.5 seconds to run the entire animation. It should do this forever (aka as long as an element has this class) alternating left and right.

Now, we'll need a way to know when we should shake and when we shouldn't. We don't want the element to be shaking forever, so, we should have some way to track if it should be shaking or not so we can render accordingly. We'll use state for that.

const [isShaking, setIsShaking] = useState(false);

Next, we'll need to use that state and the new class in the styles on the Button element.

<Button
            onClick={handleClick}
            sx={{ ...sxStyles.button, ...(isShaking && sxStyles.shake) }}>
            Click it!
        </Button>

Here we are conditionally adding the shake styles only if the element should be shaking.

Finally, we'll need to update the click handler to update the isShaking state accordingly.

const handleClick = () => {
        if (disabled) {
            setIsShaking(true);
            setTimeout(() => setIsShaking(false), 500);
        } else {
            console.log("You did it!");
        }
    };

Altogether, we'll have the following:

import { useState } from "react";
import { Button, keyframes } from "@mui/material";

type MyButtonProps = {
    disabled?: boolean;
};

const animateShake = keyframes`
    0%, 100% {
        translateX(0);
    }
    20%, 60% {
        translateX(-5px);
    }
    40%, 80% {
        translateX(5px);
    }
`;

const sxStyles = {
    button: {
        marginTop: 1,
        padding: 1,
    },
    shake: {
        animation: `${animateShake} 0.5s infinite alternate`,
    },
};

export const MyButton = ({ disabled }: MyButtonProps) => {
    const [isShaking, setIsShaking] = useState(false);

    const handleClick = () => {
        if (disabled) {
            setIsShaking(true);
            setTimeout(() => setIsShaking(false), 500);
        } else {
            console.log("You did it!");
        }
    };

    return (
        <Button
            onClick={handleClick}
            sx={{ ...sxStyles.button, ...(isShaking && sxStyles.shake) }}>
            Click it!
        </Button>
    );
};

Now you have a button that will shake when it should be disabled and do the normal thing when it's not.

I hope this example of using keyframes with React and MUI is helpful. It's a contrived example but the use of keyframes with MUI is applicable in many situations.

Happy coding!

Comments

Popular posts from this blog

A Common Technical Lead Pitfall

Maze Generation in JavaScript

Leadership Experiment Update 2