A native CSS hover text effect for React. Customizable, and it respects reduced-motion preferences.
<RotateText
text="That's a smooooth effect"
className="text-4xl font-bold"
as="h1"
/>Copy and paste the following code into your project.
import { cn } from "@/lib/utils";
import type { CSSProperties } from "react";
type RotateTextProps<T extends React.ElementType = "span"> = {
text: string;
className?: string;
animationDurationMs?: number;
staggerDelayMs?: number;
as?: T;
} & Omit<React.ComponentProps<T>, "children">;
function RotateText<T extends React.ElementType = "span">({
text,
className,
as,
animationDurationMs = 400,
staggerDelayMs = 40,
...props
}: RotateTextProps<T>) {
const chars = text.split("") as string[];
const Comp = as || "span";
const renderChars = (variant: "visible" | "hidden") =>
chars.map((char, index) => (
<span
key={index}
aria-hidden={true}
style={{
"--index": index,
"--animation-duration": `${animationDurationMs}ms`,
"--stagger-delay": `${staggerDelayMs}ms`,
} as CSSProperties}
className={cn(
"inline-block will-change-[transform,opacity,filter] transition-[transform,opacity,filter] duration-(--animation-duration) ease-linear",
"motion-reduce:transition-none motion-reduce:will-change-auto motion-reduce:[transition-delay:0s]",
variant === "visible"
? [
"[transition-delay:calc(var(--stagger-delay)*var(--index)+0.1s)]",
"group-hover:transform-[translateY(-18px)_rotateX(80deg)] group-hover:opacity-0 group-hover:blur-sm",
"group-hover:[transition-delay:calc(var(--stagger-delay)*var(--index))]",
"motion-reduce:group-hover:transform-none motion-reduce:group-hover:opacity-100 motion-reduce:group-hover:blur-none",
"motion-reduce:group-hover:[transition-delay:0s]",
]
: [
"transform-[translateY(18px)_rotateX(80deg)] opacity-0 blur-sm",
"[transition-delay:calc(var(--stagger-delay)*var(--index))]",
"group-hover:transform-[translateY(0)_rotateX(0deg)] group-hover:opacity-100 group-hover:blur-none",
"group-hover:[transition-delay:calc(var(--stagger-delay)*var(--index)+0.1s)]",
"motion-reduce:opacity-0 motion-reduce:blur-none motion-reduce:transform-none",
"motion-reduce:group-hover:opacity-0 motion-reduce:group-hover:transform-none motion-reduce:group-hover:blur-none",
"motion-reduce:group-hover:[transition-delay:0s]",
]
)}
>
{char === " " ? "\u00A0" : char}
</span>
));
return (
<Comp
{...props}
className={cn("group relative grid w-fit place-items-center", className)}
aria-label={text}
>
<div className="col-start-1 row-start-1" aria-hidden={true}>{renderChars("visible")}</div>
<div className="col-start-1 row-start-1 motion-reduce:hidden" aria-hidden={true}>{renderChars("hidden")}</div>
</Comp>
);
}
export default RotateText;It accepts all the props of the HTML element it renders as, plus the following:
| Prop | Type | Description |
|---|---|---|
text | string | The text to display. |
as | React.ElementType | The HTML element to render as. Default is span. |
animationDurationMs | number | The duration of the animation in milliseconds. Default is 400. |
staggerDelayMs | number | The delay between each character in milliseconds. Default is 40. |