A like button that likes you back

Aug 26, 2023

A like button that likes you back
// <Playground description="What we're building today">
//   <LikeButton2 slug="a-like-button-that-likes-you-back" />
// </Playground>

I'm a big fan of Josh Comeau's work. The like button on his blog is one of the most delightful UI elements I've come across. It's also a deceptively complex component (I honestly thought I could re-implement it in an hour, haha) that contains an SVG, sounds, mouse tracking, animations and of course, exploding confetti. 🎉

After reading his How I built my blog article where he talked about persisting the number of likes to a database, I knew I wanted to experiment creating something similar. I thought this would be a good example to begin my 30 day front-end challenge because it's an isolated component that connects the front-end with the back-end.

I started by writing down everything I noticed with Josh's component and ended up with a list of 15 things triggered by user interaction (i.e. onClick: changes number color from gray to pink, increments counter, scales heart, shows and fades +1 sign, etc). The attention to detail is ridiculous, but that's what makes it so good.

Although I didn't have the time to recreate everything, I thought I'd try to get the basic functionality down and maybe add some animations. Here's how my version works:

The Heart

// <Playground>

//   <LikeButtonDemo isSkeleton />
// </Playground>

At its most basic level, the like button is an SVG with two rectangles "under" a heart shaped mask. The first rectangle is filled with gray and serves as the background. The second rectangle is filled with a gradient and is what fills the heart as the user clicks. To animate the fill, I used transform and translate to position the gradient offscreen. I then filled the heart, by translating the y axis of the gradient up until the user hits the maximum number of likes (3 at most). For the purpose of the demo the gradient is also translating down so you can keep interacting. Try clicking above ☝️

I'm using framer-motion to help with the easing between each animation state. The first animation grows the hearts size on hover and click, and the second animation gently translates the gradients position.

Making it playful

<Playground>
  <LikeButtonDemo enableEmojis initialLikes={0} />
</Playground>

We can make things a little more playful and show our appreciation for the user liking our content by using the world's most expressive language: Emojis! 👍 🙏 🥰

Persisting state

<Playground description="This component persists data, like and refresh the page to see it.">
  <LikeButton2 slug="a-like-button-that-likes-you-back-db" />
</Playground>

A like button is not very useful if users can't see each other's likes or the like count is lost whenever the user leaves the page.

Because we want to limit the number of times a user can like a post: we need to keep track of two things on a post by post basis: the total number of post likes and the individual users likes.

Josh found out the hard way that persisting the individual users likes to localStorage can be circumvented after a friendly-naughty user added 40,000 likes to one of his posts. So using a database seemed like the best solution.

Each time a user likes a post, I send a POST request to a Next.js api route. The api route receives the post slug and the users IP address (which Vercel conveniently forwards).

We can use the post slug and a hashed user IP address to create a unique id. We then either create a new row if an item with that id doesn't exist, or increment the number of likes if it does. I'm using prisma (a bit overkill for two fields but love is blind) to send and retrieve the data to a PostgreSQL database hosted on Heroku.

Finishing touches

//  <Playground description="The component's loading state ☝️">

//   <LikeButtonDemo isLoading />
// </Playground>

To complete the circle, when the user first loads the page, we send a GET request to the api route to retrieve data for the post. I'm using SWR to simplify the data fetching process. With very little ceremony, SWR gives us a loading state, client side caching, revalidation on page focus and optimistic UI (increment the like count instantly while updating the database in the background).

Stretch

  • Explore adding sounds to the interactions
  • Use CSS transform/transition animations instead of framer-motion
  • Create a cuter SVG heart shape perhaps using figma
  • Debounce the POST request if a user clicks rapidly
  • Move to a database the works well with serverless. Pretty sure I will hit database connection limits on Heroku's free tier.

Conclusion

Not quite Josh Comeau level, but I had fun nonetheless. Comparing my solution to Josh's really shows how you can keep pushing even the most simple UI elements to the next level until they are a joy and delight to use.