Revelry image screenshot 2025 02 12 at 9. 39. 16 am

Valentine’s Day is almost upon us! At Revelry we take love seriously. To get into the spirit of the season I thought it would be fun to make a React CSS animation of a heart to show a loved one, because I’m a programmer, not a heart surgeon. If you’d rather dive straight into the code, you can find it on GitHub

The Heart: Crafting our SVG

You can procure a heart SVG any way you want (I won’t tell a soul), but I’m going to create our heart in figma, and then export it as an SVG. After that I’ll spin up a quick React Typescript project using Vite. I recommend using a currently active version of node/NPM.

Set Up Your React Project

First, run this command in your terminal:

npm create vite@latest hearty --template react

Select the options for react and typescript. Navigate to the directory and then install the dependencies.

cd hearty && npm i

Now, open the project in your preferred text editor (I’m using VS Code), and then open the file App.tsx.

By default, your App.tsx file will have a bunch of auto generated boilerplate, which we can clear out. We want to delete everything between the parentheses in our return statement.

Your App component should now look like this:

import "./App.css";


function App() {
 return (null);
}
export default App;

It’s important to pass that null value, because otherwise your app won’t compile (the parentheses are optional).

Styling the Heart SVG for Animation

You can now add your heart SVG file to your project as a component. I named my file Heart.tsx.

Paste the markup of your svg as the return value of the Heart component. Then, import your heart in your App.tsx file.  

At this point, the heart will be whatever color it was exported/downloaded as. To allow customization, modify the file to override the fill and stroke properties from their hardcoded values to whatever color you wish. In the Heart.tsx file, pass in two new props, fillColor, and strokeColor. 

type Props = {
  strokeColor: string;
  fillColor: string;
};

export const Heart = ({ strokeColor, fillColor }: Props) => {

Find the fill and stroke properties and replace whatever value is there with the new variables. This will allow you to use the css color property to change the color of the SVG:

fill={fillColor} stroke={strokeColor}

You could do this with one variable for the color, but this approach is more flexible. When I exported from figma, the svg also came with two rect elements that had white fills. I deleted them from my SVG file but you do you!

While in there, you will also want to find the stroke-width, stroke-linecap, and stroke-linejoin and convert those keys to be camel case (strokeWidth, strokeLinecap, and strokeLinejoin).  Otherwise, you will see a few nasty errors in the console, and things may not work as expected.

The App component code should look something like this:

import "./App.css";
import { Heart } from "./Heart";

function App() {
  return <Heart strokeColor="red" fillColor="red" />;
}

export default App;
React css animation heart

And there you have your heart SVG! But hearts don’t just like… hangout. This is Valentine’s Day after all!

CSS Animation: The Beating Heart

We’re going to utilize the power of CSS animations to make your heart more interesting. Let’s animate our heart to beat because that’s what hearts do!

In the app.css file, remove the prebuilt classes except for the rule targeting the id root (#root). This will keep your content nice and centered.

Next,  you will  make a css animation using keyframes:

@keyframes beat {
  0% {
    transform: scale(1);
  }
  50% {
    transform: scale(5);
  }
  100% {
    transform: scale(1);
  }
}

This React CSS animation uses keyframes to animate an element between different states. Use the transform property and scale function to take the heart from 1 (initial scale) to 5 (5 times initial scale) and then shrink back down to 1.

Then add a css class that uses the animation we’ve defined.

I called mine “beating”:

.beating {
  animation: beat 1s infinite;
}

Then in App.tsx, you are going to wrap your heart component in a div. And add a className property with a value of “beating” and return the div:

import "./App.css";
import { Heart } from "./Heart";

function App() {
  return (
    <div className="beating">
      <Heart strokeColor="red" fillColor="red" />
    </div>
  );
}

export default App;

And there’s the animation sorted. A beating heart is a beautiful thing, but one of the exciting things about love is that it can make your heart beat faster!

Adjusting Heartbeat Speed

Let’s add the ability to click the heart SVG and make it beat faster! 

First, you want  to refactor the code a bit. Instead of using className,use the style property, and you’re going to inline your animation rule:

<div style={{ animation: `beat ${rate}s infinite` }}>

Here, you are still using your beat animation infinitely but have replaced the value 1 for your animation duration with a rate value (thanks, template literals, for making it look so clean!).

Well, this will obviously only work if you have a rate variable right? Righhhhhhht?

So let’s use useState to add rate and setRate variables at the top of our app component.

const [rate, setRate] = useState(1);

You want to give it the initial value of 1 to reflect what you were using as your default animation. All that’s left is the click.

Finally, you will add an onClick handler to your div. You can do this in a few ways, but I prefer to bind the click event to a named function rather than pass it a callback function when possible. That’s the difference between:
onClick={beat} and onClick={() => beat()}. Pick your poison.

So your return code should now look like this:

<div style={{ animation: `beat ${rate}s infinite` }} onClick={beat}>
      <Heart strokeColor="red" fillColor="red" />
 </div>

And our beat function should look like this:

const beat = () => {
    setRate(); // what should go here?
  };

This is trickier than it might appear at first glance, because the rate here doesn’t represent the heart rate. It actually represents the rate of the animation. Increasing this value increases the length of the animation and actually makes it look like the heart is beating slower.

You can choose whatever value you like, but if you want the heart to beat faster, the new value needs to be less than the previous value.

Mine looks like this:

const beat = () => {
    setRate(rate - 0.1);
  };

The Special Someone: Adding Personalization

What’s more romantic than customizing your message for someone special? We’ll achieve this by incorporating URL params to inject a name dynamically.

We could use a library to do this like react router dom, but that’s overkill for this purpose. Instead, we’re going to use the good old window API.

First, get the search params and make a new URL search params object, like so:

const query = new URLSearchParams(window.location.search);

I’m going to look for a query param called “name”, but you can look for whatever param you want.

const name = query.get("name");

The important thing for following this implementation is understanding how your URL needs to be structured.

You’re going to use the URL structure http://localhost:5173/?name=madeline.

If you want to use multiple words, you can use the plus sign in the URL like so: http://localhost:5173/?name=my+lady. This would make the name variable have the value “my lady”

Now, we must render the message we’ll use our name with. I’m going with “you make my heart beat faster” (because I am a hopeless romantic).

For now, I’ll give it just a little margin. My message currently looks like this:

<div style={{ margin: "5rem 0" }}>you make my heart beat faster</div>

And in the full markup:

<div>

      <div style={{ animation: `beat ${rate}s infinite` }} onClick={beat}>

        <Heart fillColor="red" strokeColor="red" />

      </div>

      <div style={{ margin: "5rem 0" }}>you make my heart beat faster</div>

 </div>

You can offload the message retrieval to a function, which allows you to write more complex code that remains meaningful in the JSX markup.

Above the app component, you will declare a function getMessage, which currently takes no parameters and returns our message. The reason to declare it outside of the app component is that it doesn’t rely on any state of the app component. This prevents the function from being recreated on each render.

Your getMessage function should look like this right now:

const getMessage = () => {

  return "you make my heart beat faster";

};

And your usage:

<div style={{ margin: "5rem 0" }}>{getMessage()}</div>

Now, let’s update the function to take the name parameter.

Your getMessage function should look like this:

const getMessage = (name: string | null) => {

  if (name) return `${name}, you make my heart beat faster!`;

  return "You make my heart beat faster!";

};

The above code will handle cases where the name has a value or is null. For this usage, we pass the name into the getMessage call like so

<div style={{ margin: "5rem 0" }}>{getMessage(name)}</div> 

Final Touches: Preventing a Heart Attack (Literally)


This is mostly what we want, but there are some enhancements we can make.

First, you may have noticed your message in the get message function is written twice (once with a capital Y and once with a lowercase y).

Let’s save the lowercase version in a variable:

const getMessage = (name: string | null) => {

  const message = "you make my heart beat faster!";

  if (name) return `${name}, ${message}`;

  return message;

};

With this implementation, you only need to update the message variable if you decide to change your message—for example, “I choo choo choose you.”. The drawback is that if no name is provided, the message starts with a lowercase letter!

So, let’s make a minor update to handle the cases ahead of us efficiently.

The following implementation gets most of the way there:

const getMessage = (name: string | null) => {

  let message = "you make my heart beat faster!";

  if (name) {

    message = `${name}, ${message}`;

  }

  return message;

};

However, the first letter still isn’t capitalized.  One way to fix this is to use JS to cast the first character to upper case and combine it with a copy of the rest of the string using slice. But there’s a more straightforward approach we can employ that uses CSS. 

In the App.css file, declare this class pseudo selector:

.message::first-letter {

  text-transform: capitalize;

}

And attach it to the message div.

<div className="message" style={{ margin: "5rem 0" }}>

        {getMessage(name)}

      </div>

And while you’re at it, add the margin style to a message class in the CSS file:

.message {

  margin: 5rem 0

}

And now, you can just delete your style rule!

The last hiccup to address is that it’s possible to make our heart beat so fast that it stops beating entirely… which, while poetic, isn’t the intended effect. So let’s put a cap on the clicking so that our animation doesn’t cause a coronary:

const beat = () => {

    const updatedRate = rate - 0.1;

    if (updatedRate) {

      setRate(updatedRate);

    } else {

      setRate(1);

    }

  };

Decrease the rate by 0.1, and if the updated rate has a truthy value ( meaning it’s not 0), it should set the new rate. Otherwise, the rate resets to its starting value. PERFECT!

But not so fast! This implementation introduces a bug due to the famous JS floating point math. The math doesn’t work and will never actually hit 0

So, we must do the math, convert the number to a fixed point stringified version, and then parse it back into the number all in a single  line of code:

const updatedRate = parseFloat((rate - 0.1).toFixed(1));

The Long Kiss Goodnight

And there you have it! A fun little coding project to impress that special someone this Valentine’s Day. 

I’d love to know what modifications or improvements you make to your implementation. Maybe you can use an anatomical heart or add a feature I didn’t think of. 

This project is your playground, but there is one rule: you have to share the cool stuff you create–it’s Valentine’s Law!

♥️

Checkout some of our other React insights.

We're building an AI-powered Product Operations Cloud, leveraging AI in almost every aspect of the software delivery lifecycle. Want to test drive it with us? Join the ProdOps party at ProdOps.ai.