Hover Animations on Terminal-like Typography.
Tip
This component is best viewed on desktop.
npm install framer-motion
"use client";
import React, { useState, useRef, useEffect } from "react";
import { animate, motion, useAnimation, AnimationControls } from "framer-motion";
import { cn } from "@/lib/utils";
const lettersAndSymbols = [
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o",
"p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "!", "@", "#", "$",
"%", "^", "&", "*", "-", "_", "+", "=", ";", ":", "<", ">", ","
];
interface TerminalCharProps {
char: string;
delay: number;
trigger?: boolean;
}
interface TerminalTextProps {
trigger: boolean;
children: string;
}
interface TerminalTextHoverEffectProps {
children: string;
className?: string;
}
const TerminalChar: React.FC<TerminalCharProps> = ({ char, delay, trigger }) => {
const controls: AnimationControls = useAnimation();
const originalChar = useRef<string>(char);
const charRef = useRef<HTMLDivElement>(null);
const [currentChar, setCurrentChar] = useState<string>(char);
const [blockOpacity, setBlockOpacity] = useState<number>(0);
const [timeoutID, setTimeoutID] = useState<NodeJS.Timeout | undefined>();
const animation = async (): Promise<void> => {
await controls.start({
opacity: [0, 1],
transition: { duration: 0.1 },
});
for (let i = 0; i < 3; i++) {
setCurrentChar(
lettersAndSymbols[Math.floor(Math.random() * lettersAndSymbols.length)]
);
if (i === 0) {
setBlockOpacity(0);
}
await new Promise((resolve) => setTimeout(resolve, 40));
}
setCurrentChar(originalChar.current);
};
useEffect(() => {
if (trigger) {
clearTimeout(timeoutID);
if (charRef.current) {
animate(charRef.current, { opacity: 0 });
}
setTimeoutID(setTimeout(() => animation(), delay * 1000));
}
}, [trigger, delay]);
return (
<motion.div
animate={controls}
onAnimationStart={() => setBlockOpacity(1)}
ref={charRef}
className={`
inline-block leading-[0.9] uppercase relative after:content-['']
after:w-[1ch] after:absolute after:top-0 after:left-0
after:bg-foreground after:h-full ${
blockOpacity === 0 ? "after:opacity-0" : "after:opacity-1"
}`}
>
{currentChar}
</motion.div>
);
};
const TerminalText: React.FC<TerminalTextProps> = ({ trigger, children }) => {
return (
<span className="hover-effect cursor-pointer group">
{Array.from(children).map((char, index) => (
char === " " ? (
<div key={index} className="inline-block">{"\u00A0"}</div>
) : (
<TerminalChar
key={index}
char={char}
delay={index * 0.07}
trigger={trigger}
/>
)
))}
</span>
);
};
const TerminalTextHoverEffect: React.FC<TerminalTextHoverEffectProps> = ({ children, className }) => {
const [isHovered, setIsHovered] = useState<boolean>(false);
const handleMouseEnter = (): void => setIsHovered(true);
const handleMouseLeave = (): void => setIsHovered(false);
return (
<div
className={cn("flex items-start font-mono", className)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<TerminalText trigger={isHovered}>{children}</TerminalText>
</div>
);
};
export default TerminalTextHoverEffect;
Prop | Type | Default | Description |
---|---|---|---|
children | string | undefined | The string that you want the hover effect on |
className | string | undefined | Additional CSS classes to apply to the div |