Thoughtful micro-interactions can transform a static interface into something more welcoming and human. In this project, I built a hand-wave animation component using React, Motion, and Tailwind CSS to demonstrate how subtle motion can improve user experience without overwhelming the design.
In this post, we’ll create a reusable animated SVG component using Motion to handle animation states and transitions.
Objective
- Animate a standard SVG icon using Motion
- Keep the animation smooth and natural
- Make the component reusable across different parts of an application
Tech Stack
Next.jsApp RouterMotionfor animation controlTailwindCSS for styling and sizingSVGfor scalable, crisp visuals
Component Structure
The animation is applied directly to the SVG itself using motion.svg.
<motion.svg
fill="none"
viewBox="0 0 16 16"
className={cn("size-4", className)}
>Instead of using a regular SVG element with additional CSS keyframe animations, this component uses motion.svg, which provides built-in animation capabilities such as initial, animate, and transition props.
Styling and Reusability
The SVG uses currentColor, allowing it to inherit text color from its parent.
Sizing is handled via Tailwind size-4 by default, and the optional className prop allows full customization.
Wave Animation Logic
The waving motion is created using a sequence of rotation values combined with subtle scaling:
animate={{
rotate: [0, 20, -10, 20, -5, 0],
scale: [1, 1.2, 1.2, 1],
}}Instead of a single rotation, multiple angles are used to simulate a natural hand wave. The slight scale increase adds a sense of energy without being distracting.
Timing and Loop Behavior
transition={{
duration: 1,
repeat: Infinity,
times: [0, 0.2, 0.8, 1],
repeatDelay: 2,
ease: "easeInOut",
}}- 1 second per wave
- Infinite looping
- 2 second pause between waves
- Smooth easing for a natural feel
This timing ensures the animation feels friendly and intentional, not repetitive or noisy.
Live Demo: How It Feels in the UI
Here’s the complete code
"use client";
import { cn } from "@/lib/utils";
import { motion } from "motion/react";
type WaveIconProps = {
className?: string;
};
const WaveIcon = ({ className }: WaveIconProps) => {
return (
<motion.svg
fill="none"
viewBox="0 0 16 16"
initial={{ scale: 1 }}
animate={{ rotate: [0, 20, -10, 20, -5, 0], scale: [1, 1.2, 1.2, 1] }}
transition={{
duration: 1,
repeat: Infinity,
times: [0, 0.2, 0.8, 1],
repeatDelay: 2,
ease: "easeInOut",
}}
className={cn("size-4 inline-flex", className)}
>
<g stroke="currentColor" strokeWidth="1.33333">
<path d="m10.6812 6.48941-2.97055-5.15608-1.14342.66152c-.63148.36535-.84784 1.17452-.48326 1.80732m4.59723 2.68724.9902 1.71872-.5719.33086c-.9472.548-1.27179 1.76181-.7249 2.71101l.3301.5729m-.0235-5.33349c-.3646-.63281-.1482-1.44198.4833-1.80732l1.1433-.66153 1.6504 2.8645c1.4584 2.53127.5929 5.76794-1.933 7.22934l-.5716.3307m-5.36963-10.64293-1.32027-2.29159-1.14339.66153c-.63148.36535-.84784 1.17451-.48325 1.80732m2.94691-.17726 2.31048 4.01029m-5.25739-3.83303-1.14339.66152c-.63148.36535-.84784 1.17452-.48325 1.80732l4.73492 8.21843m-3.10828-10.68727 2.97061 5.1561" />
<path d="m.666687 11.3333c0 1.0609.421423 2.0783 1.171573 2.8284.75015.7502 1.76756 1.1716 2.82843 1.1716" />
<path d="m15.3333 4.66667c0-1.06086-.4214-2.07828-1.1716-2.82842-.7501-.75015-1.7675-1.171578-2.8284-1.171578" />
</g>
</motion.svg>
);
};
export default WaveIcon;Notice the use client directive at the top of the file. This is necessary because
Motion uses browser APIs that are unavailable in server components.
Final Thoughts
This hand-wave animation is a small but intentional detail designed to add personality without distraction. By combining Motion’s animation control with Tailwind’s styling flexibility, the result is a clean, expressive UI component suitable for real-world production use.
Micro-interactions like this can make interfaces feel more approachable—and often, that’s what users remember most.