Text Highlighter
An animated text highlighting component with multiple trigger modes and directional animations.
Installation
1pnpm dlx shadcn add @fancy/text-highlighter
Usage
Just wrap your text content with the component and set the highlight color with the highlightColor prop.
Usage example
1<TextHighlighter highlightColor="hsl(25, 90%, 80%)">Howdy!</TextHighlighter>
Understanding the component
The magic behind this component lies in animating the text's background. Instead of using a solid background color (CSS prop: background-color), we use background-image with a linear gradient. This allows us to animate the entire background of the text by changing the background-size property; something that wouldn't be possible with the simple background-color property. We also use a linear gradient because we can't set a solid color directly as a background image (as far as I know).
Highlighter style
1const highlightStyle = {2 backgroundImage: `linear-gradient(${highlightColor}, ${highlightColor})`,3 backgroundRepeat: "no-repeat",4 backgroundPosition: backgroundPosition,5 backgroundSize: animatedSize,6 boxDecorationBreak: "clone",7 WebkitBoxDecorationBreak: "clone",8} as React.CSSProperties
We also use box-decoration-break: clone to make sure each individual line is properly highlighted when dealing with multi-line text. Check out this demo why this is important.
The direction of the highlight reveal is controlled by the direction prop. Depending on the value, we set the background-position and background-size accordingly. There is two function which returns the appropriate values:
Get animation values by direction
1// Get background size based on direction2const getBackgroundSize = (animated: boolean) => {3 switch (currentDirection) {4 case "ltr":5 return animated ? "100% 100%" : "0% 100%"6 case "rtl":7 return animated ? "100% 100%" : "0% 100%"8 case "ttb":9 return animated ? "100% 100%" : "100% 0%"10 case "btt":11 return animated ? "100% 100%" : "100% 0%"12 default:13 return animated ? "100% 100%" : "0% 100%"14 }15}1617// Get background position based on direction18const getBackgroundPosition = () => {19 switch (currentDirection) {20 case "ltr":21 return "0% 0%"22 case "rtl":23 return "100% 0%"24 case "ttb":25 return "0% 0%"26 case "btt":27 return "0% 100%"28 default:29 return "0% 0%"30 }31}
Then, we just use motion to animate the background-size property based on the shouldAnimate state:
Animation
1<motion.span2 className={cn("inline", className)}3 style={highlightStyle}4 animate={{5 backgroundSize: animatedSize,6 }}7 initial={{8 backgroundSize: initialSize,9 }}10 transition={transition}11>12 {children}13</motion.span>
You can customize the transition by passing a Transition object to the transition prop. The default value is spring type animation { type: "spring", duration: 1, delay: 0., bounce: 0 }.
By default, the animation will be triggered once the component is mounted. Another interesting trigger option is inView, which will trigger the animation when the component enters the viewport (demonstrated in the demo above). You can customize that behaviour by setting the useInViewOptions prop. For more information, check out the useInView documentation.
Different directions
You can control the highlight animation direction via the direction prop. The available options are:
"ltr"- Left to right animation"rtl"- Right to left animation"ttb"- Top to bottom animation"btt"- Bottom to top animation
The following demo shows how to dynamically change the reveal direction based on the user's scroll direction. Scroll left and right to see the animations trigger.
Hover
You can also trigger the highlight animation via hover, if you set the triggerType prop to "hover":
Control via ref
You can also trigger the animation via an exposed ref. This is useful if you want to trigger the animation programmatically:
Notes
- While the component only support a single-colored highlight directly, you can change it to an image, a fancy gradient, or anything that a
background-imagecan handle. Just change the appropriate line:
Fancier highlight color
1backgroundImage: `linear-gradient(${highlightColor}, ${highlightColor})`, // change this to make it fancier
- As many users have pointed out, excessive animations can be distracting and impact readability, especially when highlighting large blocks of text. Consider using animations sparingly and adjusting the transition duration and delay to create a more subtle effect. You may also want to use the
useInViewOptionsprop to control when animations trigger, for example by increasing theamountthreshold or settingonce: trueto only animate elements once.
Props
TextHighlighterProps
| Prop | Type | Default | Description |
|---|---|---|---|
| children* | React.ReactNode | - | The text content to be highlighted |
| as | ElementType | "span" | HTML element to render as |
| triggerType | "auto" | "hover" | "ref" | "inView" | "inView" | How to trigger the animation |
| transition | Transition | { type: "spring", duration: 1, delay: 0, bounce: 0 } | Animation transition configuration |
| useInViewOptions | UseInViewOptions | { once: true, initial: false, amount: 0.5 } | Options for useInView hook when triggerType is "inView" |
| className | string | - | Class name for the container element |
| highlightColor | string | "hsl(25, 90%, 80%)" | Highlight color (CSS color string) |
| direction | "ltr" | "rtl" | "ttb" | "btt" | "ltr" | Direction of the highlight animation |
TextHighlighterRef
| Method | Description |
|---|---|
| animate(direction?: HighlightDirection) | Trigger the highlight animation with optional direction override |
| reset() | Reset the highlight animation to its initial state |