Code Talk: How the Pickleball Text Effect Was Created on My Website



On the 'Pickleball' page on my personal website, I wanted to add a bouncing back 'n forth effect to mimic to the motion of Pickleball.
In this post, I want to go over how I made the component.
The Purpose
For every page on the 'sidequest' subpage on my website, I want to add something that is thematically relevant of the activity on the page. In this case, I needed to create a component that demonstrates the back 'n forth motion of pickleball.
Code
/**
* PingPongEffect component
* A client component that displays text with a ping-pong/pickleball effect on a specific letter
* within a target word. The letter appears to bounce back and forth like a ping pong ball
*/
"use client";
import React, { useState, useEffect } from "react";
/**
* Props for the PingPongEffect component
* @property {string} text - The complete text content to display
* @property {string} targetWord - The specific word containing the letter to animate
* @property {string} targetLetter - The letter within the targetWord that should have the ping-pong effect
* @property {string} [className] - Optional CSS class name for styling the container paragraph
*/
interface PingPongEffectProps {
text: string;
targetWord: string;
targetLetter: string;
className?: string;
}
/**
* This renders text with a ping-pong effect applied to a specific letter within a word
* The animation stops after 5 seconds
* @param {Object} props - Component props
* @param {string} props.text - The full text to display
* @param {string} props.targetWord - The word containing the letter to animate
* @param {string} props.targetLetter - The letter to apply the ping-pong animation to
* @param {string} [props.className] - Optional CSS class for styling
* @returns - The rendered text with a ping-pong effect on the specified letter
*/
export default function PingPongText({
text,
targetWord,
targetLetter,
className,
}: PingPongEffectProps): React.JSX.Element {
// State to track whether animation is active
const [isAnimating, setIsAnimating] = useState(true);
// Effect to stop animation after 5 seconds
useEffect(() => {
const timer = setTimeout(() => {
setIsAnimating(false);
}, 5000);
// Clean up timer if component unmounts
return () => clearTimeout(timer);
}, []);
// If targetWord is not in text, just return the text
if (!text.includes(targetWord)) {
return <p className={className}>{text}</p>;
}
// Split the text by the targetWord
const parts = text.split(targetWord);
// Create an animated version of the target word
const renderAnimatedWord = () => {
// Find the position of the target letter in the word
const letterPos = targetWord.indexOf(targetLetter);
// If letter not found in word, return the word as is
if (letterPos === -1) {
return targetWord;
}
return (
<>
{targetWord.substring(0, letterPos)}
<span
className={isAnimating ? "pingpong-letter" : "pingpong-letter-static"}
>
{targetLetter}
</span>
{targetWord.substring(letterPos + 1)}
</>
);
};
return (
<p className={className}>
{parts.map((part, index) => (
<React.Fragment key={index}>
{part}
{index < parts.length - 1 && renderAnimatedWord()}
</React.Fragment>
))}
<style jsx global>{`
@keyframes pingPongBounce {
0% {
transform: translateY(0) translateX(0);
border-radius: 50%;
}
25% {
transform: translateY(-5px) translateX(6px);
}
50% {
transform: translateY(0) translateX(0);
}
75% {
transform: translateY(-5px) translateX(-6px);
}
100% {
transform: translateY(0) translateX(0);
border-radius: 50%;
}
}
.pingpong-letter {
display: inline-block;
position: relative;
color: red;
animation: pingPongBounce 1.4s infinite ease-in-out;
padding: 0 2px;
font-weight: bold;
border-radius: 50%;
transition: color 1.4s ease-out, transform 4.4s ease-out;
}
.pingpong-letter-static {
display: inline-block;
position: relative;
// padding: 0 2px;
// font-weight: bold;
color: inherit;
border-radius: 50%;
transition: color 1.4s ease-out, transform 4.4s ease-out;
transform: translateY(0) translateX(0);
}
`}</style>
</p>
);
}
Full Disclaimer: I do recognize that the function is called PingPongEffectProps
. Ping pong did come first in my life, and at the time I was thinking about ping pong in my head. Also, I always called pickleball—enlarged ping pong.
The API
The component accepts four props:
- •
text
: The complete text content to display. - •
targetWord
: The specific word containing the letter to animate. - •
targetLetter
: The letter within the targetWord that should have the ping-pong effect. - •
className
: Optional CSS class name for styling the container paragraph.
/**
* Props for the PingPongEffect component
* @property {string} text - The complete text content to display
* @property {string} targetWord - The specific word containing the letter to animate
* @property {string} targetLetter - The letter within the targetWord that should have the ping-pong effect
* @property {string} [className] - Optional CSS class name for styling the container paragraph
*/
interface PingPongEffectProps {
text: string;
targetWord: string;
targetLetter: string;
className?: string;
}
Usage
The component is dead-simple to use:
<PingPongText
text={metadata.description?.toString() ?? ""}
targetWord="courts"
targetLetter="o"
className="mb-8 text-neutral-600 dark:text-neutral-400"
/>
The Animation
The component captures the "ping pong" and "pickleball" feel from a CSS animation applied to one single letter. In the example below, you will see the keyframes:
@keyframes pingPongBounce {
0% {transform: translateY(0) translateX(0); border-radius: 50%;}
25% {transform: translateY(-5px) translateX(6px);}
50% {transform: translateY(0) translateX(0);}
75% {transform: translateY(-5px) translateX(-6px);}
100% {transform: translateY(0) translateX(0); border-radius: 50%;}
}
This was a fun one to design, not going to lie. The smooth easing and short interval I put in place gives a cool side-to-side bounce. The animation runs for about 5 seconds after the component mounts and then fades out.
The animated and static styles look like this:
.pingpong-letter {
display: inline-block;
position: relative;
color: red;
animation: pingPongBounce 1.4s infinite ease-in-out;
padding: 0 2px;
font-weight: bold;
border-radius: 50%;
transition: color 1.4s ease-out, transform 4.4s ease-out;
}
.pingpong-letter-static {
display: inline-block;
position: relative;
// padding: 0 2px;
// font-weight: bold;
color: inherit;
border-radius: 50%;
transition: color 1.4s ease-out, transform 4.4s ease-out;
transform: translateY(0) translateX(0);
}
Component Design
This component was written in a monolithic style. All the logic, rendering, animation, and styling is kept within one file. This component is a one-time use component. I am not planning on using it again and most likely will not use the logic inside of there so there was no need to make this compositional.
At render time, the component:
- Splits the full
text
at every occurance oftargetWord
. - Reconstructs the string and inserts an animated version of
targetLetter
within each occurrence. - Uses a
useEffect
hook to deactivate the animation after about 5 seconds.
If the targetWord
is not found in the text, it returns the plain text instead.