A pure CSS lift-on-hover text effect for React. Fully customizable with Tailwind CSS and no animation library required.
import { LiftText } from "@/components/lift-text";
function LiftTextDemo() {
return (
<>
<LiftText
aria-label="Default"
className="font-bold font-sans text-5xl uppercase leading-none no-underline"
text="Default"
/>
<LiftText
aria-label="Smaller delay"
className="font-bold font-sans text-5xl uppercase leading-none no-underline"
delaySec={0.005}
text="Smaller delay"
/>
<LiftText
aria-label="I go Down"
className="font-bold font-sans text-5xl uppercase leading-none no-underline"
direction="down"
text="I go Down"
/>
<LiftText
aria-label="Link"
as="a"
className="font-bold font-sans text-5xl uppercase leading-none no-underline"
href="#"
text="That's a link"
/>
</>
);
}Copy and paste the following code into your project.
import { cn } from "@/lib/utils";
interface LiftTextProps<T extends React.ElementType = "p"> {
as?: T;
delaySec?: number;
direction?: "up" | "down";
text: string;
}
function LiftText<T extends React.ElementType = "p">({
text,
as,
className,
style,
direction = "up",
delaySec = 0.02,
...props
}: LiftTextProps<T> & Omit<React.ComponentProps<T>, "children">) {
const Tag = (as ?? "p") as React.ElementType;
return (
<div className="overflow-hidden">
<Tag
className={cn("group duration-300", className)}
style={{
textShadow: direction === "up" ? "0 1em 0" : "0 -1em 0",
...style,
}}
{...props}
>
{[...text.split("").entries()].map(([position, char]) => {
if (char === " ") {
return <span key={`space-${position}`}> </span>;
}
return (
<span
aria-hidden="true"
className={cn(
"relative inline-block transition-transform duration-[inherit] ease-[cubic-bezier(0.4,0.36,0,1)] motion-reduce:transition-none",
"[transition-delay:calc((sibling-count()-sibling-index())*attr(data-delay_s))]",
"group-hover:[transition-delay:calc(sibling-index()*attr(data-delay_s))]",
"motion-reduce:group-hover:translate-y-0",
direction === "up"
? "group-hover:-translate-y-full"
: "group-hover:translate-y-full"
)}
data-delay={delaySec}
key={`char-${position}-${char}`}
>
{char}
</span>
);
})}
</Tag>
</div>
);
}
export { LiftText };import { LiftText } from "@/components/lift-text";<LiftText text="Work with us" /><LiftText text="Work with us" as="button" onClick={() => alert("Hello")} /><LiftText text="Work with us" direction="down" /> // Up by defaultThis components use the em, What's that?? So em is a text size unit relative to the font-size of the parent element. If the parent element has a font-size of 16px, and a child has font-size of 0.5em, it will be 8px. 2em it's 32px, etc.
But what happens when you use it on text-shadow? Well, it's simple. The em unit becomes relative to the current element itself. This allows the shadow size to always matching the text size where it's applied.
It inherits all the props of the native HTML tag specified by the as prop, except for the children prop. Additionally, it accepts the following props:
| Prop | Type | Default Value | Description |
|---|---|---|---|
| text | string | - | The text to be displayed |
| direction | "up" | "down" | "up" | Lift direction on hover |
| as | React.ElementType | "p" | The HTML tag to be used for the component |