Create a True Focus Text Effect in React
Blur all words in a sentence except the one currently in focus, directing attention word by word as the user reads.
Reading comprehension tools have known for a while that isolating words helps focus. True Focus borrows that idea and turns it into a UI animation. Every word is blurred out except the one in the spotlight. It is striking, and oddly meditative.
The final result
TrueFocus splits a sentence into words and blurs all of them except the currently active word. A focus box with glowing corner brackets tracks the active word with a smooth animation. In auto mode it advances on a timer; in manual mode it follows the cursor.
Setting up
This component uses Motion for the animated focus box.
import { useEffect, useRef, useState } from 'react';
import { motion } from 'motion/react';Building the component
The state tracks the current word index and a rect object that describes where the focus box should sit.
const [currentIndex, setCurrentIndex] = useState<number>(0);
const [focusRect, setFocusRect] = useState<FocusRect>({ x: 0, y: 0, width: 0, height: 0 });In auto mode, a setInterval advances the index through the words at a pace determined by animationDuration plus pauseBetweenAnimations.
useEffect(() => {
if (!manualMode) {
const interval = setInterval(() => {
setCurrentIndex(prev => (prev + 1) % words.length);
}, (animationDuration + pauseBetweenAnimations) * 1000);
return () => clearInterval(interval);
}
}, [manualMode, animationDuration, pauseBetweenAnimations, words.length]);When the index changes, we read the bounding rect of the active word span, relative to the container, and store it in state.
useEffect(() => {
if (!wordRefs.current[currentIndex] || !containerRef.current) return;
const parentRect = containerRef.current.getBoundingClientRect();
const activeRect = wordRefs.current[currentIndex]!.getBoundingClientRect();
setFocusRect({
x: activeRect.left - parentRect.left,
y: activeRect.top - parentRect.top,
width: activeRect.width,
height: activeRect.height,
});
}, [currentIndex, words.length]);Each word gets a CSS filter: blur() based on whether it is active. The transition is animated with a CSS transition property.
{words.map((word, index) => {
const isActive = index === currentIndex;
return (
<span
key={index}
ref={el => { wordRefs.current[index] = el; }}
style={{
filter: isActive ? 'blur(0px)' : `blur(${blurAmount}px)`,
transition: `filter ${animationDuration}s ease`,
}}
>
{word}
</span>
);
})}The focus box is a motion.div that animates its x, y, width, and height based on the stored rect. Four corner bracket span elements inside it create the glowing corners.
<motion.div
className='absolute top-0 left-0 pointer-events-none'
animate={{ x: focusRect.x, y: focusRect.y, width: focusRect.width, height: focusRect.height }}
transition={{ duration: animationDuration }}
>
<span className='absolute w-4 h-4 border-[3px] rounded-[3px] top-[-10px] left-[-10px] border-r-0 border-b-0'
style={{ borderColor: borderColor, filter: 'drop-shadow(0 0 4px var(--border-color))' }}
/>
{/* ... three more corners */}
</motion.div>How to use it
<TrueFocus
sentence="Focus on what matters"
manualMode={false}
blurAmount={5}
borderColor="green"
glowColor="rgba(0, 255, 0, 0.6)"
animationDuration={0.5}
pauseBetweenAnimations={1}
/>Set manualMode={true} to let users hover over words to control focus themselves.
Key takeaways
- Reading bounding rects relative to the container (not the viewport) keeps the focus box positioned correctly even when the component is not at the top of the page.
- Using a single animated
motion.divthat moves to the active word's position is far simpler than animating a border on each word individually. - The corner bracket design is purely CSS absolute positioning with selective border sides hidden, no SVG needed.