Back to library
text
Text Highlight
A marker-style background sweeps behind text on hover, in view, or via ref — with ltr, rtl, ttb and btt directions.
Inspired by Cnippet UI · View source
Preview
Ship faster with beautifully animated components.
Hover this bottom-to-top highlight.
Trigger me
Copy for AI
Copy for Lovable is the fastest way to move this component into another Lovable project — it copies the full prompt with dependencies, files, and a usage example. Copy all files gives you the raw source if you want to paste it manually.
Installation
npx shadcn@latest add @cnippet/text-highlightPaste into a terminal — or paste into a Lovable chat and I'll run it for you.
Source files
components/ui/text-highlight.tsx
"use client";
import {
motion,
type Transition,
type UseInViewOptions,
useInView,
} from "motion/react";
import {
type ElementType,
forwardRef,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from "react";
import { cn } from "@/lib/utils";
export type TextHighlightDirection = "ltr" | "rtl" | "ttb" | "btt";
export type TextHighlightTrigger = "hover" | "ref" | "inView" | "auto";
export type TextHighlightRef = {
animate: (direction?: TextHighlightDirection) => void;
reset: () => void;
};
export type TextHighlightProps = {
children: React.ReactNode;
as?: ElementType;
triggerType?: TextHighlightTrigger;
transition?: Transition;
useInViewOptions?: UseInViewOptions;
className?: string;
highlightColor?: string;
direction?: TextHighlightDirection;
} & React.HTMLAttributes<HTMLElement>;
function getBackgroundSize(
direction: TextHighlightDirection,
animated: boolean,
): string {
if (direction === "ttb" || direction === "btt")
return animated ? "100% 100%" : "100% 0%";
return animated ? "100% 100%" : "0% 100%";
}
function getBackgroundPosition(direction: TextHighlightDirection): string {
switch (direction) {
case "rtl":
return "100% 0%";
case "btt":
return "0% 100%";
default:
return "0% 0%";
}
}
export const TextHighlight = forwardRef<TextHighlightRef, TextHighlightProps>(
(
{
children,
as = "span",
triggerType = "inView",
transition = { bounce: 0, delay: 0, duration: 1, type: "spring" },
useInViewOptions = { amount: 0.1, initial: false, once: true },
className,
highlightColor = "hsl(25, 90%, 80%)",
direction = "ltr",
...props
},
ref,
) => {
const componentRef = useRef<HTMLDivElement>(null);
const [isAnimating, setIsAnimating] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const [currentDirection, setCurrentDirection] =
useState<TextHighlightDirection>(direction);
useEffect(() => {
setCurrentDirection(direction);
}, [direction]);
const inViewResult = useInView(componentRef, useInViewOptions);
const isInView = triggerType === "inView" ? inViewResult : false;
useImperativeHandle(ref, () => ({
animate: (dir?: TextHighlightDirection) => {
if (dir) setCurrentDirection(dir);
setIsAnimating(true);
},
reset: () => setIsAnimating(false),
}));
const shouldAnimate =
triggerType === "hover"
? isHovered
: triggerType === "inView"
? isInView
: triggerType === "ref"
? isAnimating
: triggerType === "auto";
const animatedSize = useMemo(
() => getBackgroundSize(currentDirection, shouldAnimate),
[shouldAnimate, currentDirection],
);
const initialSize = useMemo(
() => getBackgroundSize(currentDirection, false),
[currentDirection],
);
const bgPosition = useMemo(
() => getBackgroundPosition(currentDirection),
[currentDirection],
);
const ElementTag = as || "span";
return (
<ElementTag
onMouseEnter={() => triggerType === "hover" && setIsHovered(true)}
onMouseLeave={() => triggerType === "hover" && setIsHovered(false)}
ref={componentRef}
{...props}
>
<motion.span
animate={{ backgroundSize: animatedSize }}
className={cn("inline", className)}
initial={{ backgroundSize: initialSize }}
style={{
backgroundImage: `linear-gradient(${highlightColor}, ${highlightColor})`,
backgroundPosition: bgPosition,
backgroundRepeat: "no-repeat",
backgroundSize: animatedSize,
boxDecorationBreak: "clone",
WebkitBoxDecorationBreak: "clone",
}}
transition={transition}
>
{children}
</motion.span>
</ElementTag>
);
},
);
TextHighlight.displayName = "TextHighlight";
Dependencies
motion