Wave Text

Letters dance in a smooth sine wave pattern, creating a playful, ocean-like motion. Perfect for adding life to headers and interactive elements.

Continuous Wave

Letters continuously wave up and down in a mesmerizing pattern.

Hover Activated

Wave animation triggers on hover for interactive elements.

Subtle Motion

Gentle, barely-there motion for elegant, professional designs.

Installation

npm install framer-motion
pnpm install framer-motion
yarn add framer-motion
bun add framer-motion

Copy and paste the following code into your project.

'use client';

import { useEffect, useRef, useState } from 'react';
import { motion, useInView } from 'framer-motion';

interface WaveTextProps {
  text: string;
  className?: string;
  amplitude?: number;
  frequency?: number;
  speed?: number;
  direction?: 'up' | 'down' | 'both';
  trigger?: 'mount' | 'hover' | 'inView' | 'continuous';
  duration?: number;
}

export const WaveText = ({
  text,
  className = '',
  amplitude = 20,
  frequency = 0.3,
  speed = 2,
  direction = 'both',
  trigger = 'continuous',
  duration = 3,
}: Readonly<WaveTextProps>) => {
  const containerRef = useRef<HTMLSpanElement>(null);
  const isInView = useInView(containerRef, { once: false, amount: 0.5 });
  const [isHovering, setIsHovering] = useState(false);
  const [hasTriggered, setHasTriggered] = useState(false);

  const shouldAnimate = () => {
    switch (trigger) {
      case 'continuous':
        return true;
      case 'mount':
        return true;
      case 'inView':
        return isInView;
      case 'hover':
        return isHovering;
      default:
        return false;
    }
  };

  useEffect(() => {
    if (trigger === 'mount' && !hasTriggered) {
      setHasTriggered(true);
    }
  }, [trigger, hasTriggered]);

  const characters = text.split('');

  const getYAnimation = (index: number) => {
    const baseY = Math.sin(index * frequency) * amplitude;

    if (direction === 'up') {
      return [0, -Math.abs(baseY), 0];
    } else if (direction === 'down') {
      return [0, Math.abs(baseY), 0];
    } else {
      return [0, baseY, 0, -baseY, 0];
    }
  };

  return (
    <motion.span
      ref={containerRef}
      className={`inline-block ${className}`}
      onMouseEnter={() => setIsHovering(true)}
      onMouseLeave={() => setIsHovering(false)}
    >
      {characters.map((char, index) => (
        <motion.span
          key={`${char}-${index}`}
          className="inline-block"
          style={{
            whiteSpace: char === ' ' ? 'pre' : 'normal',
          }}
          animate={
            shouldAnimate() && char !== ' '
              ? {
                  y: getYAnimation(index),
                }
              : {}
          }
          transition={
            shouldAnimate()
              ? {
                  duration: speed,
                  ease: 'easeInOut',
                  repeat: trigger === 'continuous' ? Infinity : 0,
                  repeatType: 'loop',
                  delay: index * (frequency / 2),
                }
              : {}
          }
        >
          {char}
        </motion.span>
      ))}
    </motion.span>
  );
};

export default WaveText;

Props

PropTypeDefault
text?
string
required
amplitude?
number
20
frequency?
number
0.3
speed?
number
2
direction?
'up' | 'down' | 'both'
'both'
trigger?
'mount' | 'hover' | 'inView' | 'continuous'
'continuous'
className?
string
''

Examples

// Energetic wave
<WaveText
  text="Party Time"
  amplitude={30}
  frequency={0.6}
  speed={1}
/>

// Calm ocean wave
<WaveText
  text="Peaceful"
  amplitude={10}
  frequency={0.2}
  speed={4}
  direction="up"
/>

// Interactive hover wave
<WaveText
  text="Touch Me"
  trigger="hover"
  amplitude={20}
/>