INTERACTION_078CSS
Keyword Emphasis Animation
Key terms in a paragraph brighten after the copy settles.
Copy / LightView
A thin progress rule tracks how far the reader has moved. Scaled to 0 on the X axis before the trigger; scaling the bar to match scroll progress once scroll position within the article.
/* Reading Progress Highlight — A thin progress rule tracks how far the reader has moved. */
.reading-progress-highlight {
--dur: scroll-linked;
--ease: linear;
--stagger: 60ms;
will-change: transform, opacity;
}
.reading-progress-highlight > * {
opacity: 0;
transform: translateY(14px);
animation: reading-progress-highlight-in var(--dur) var(--ease) forwards;
animation-delay: calc(var(--i, 0) * var(--stagger));
}
/* scaled to 0 on the X axis -> scaling the bar to match scroll progress */
@keyframes reading-progress-highlight-in {
to {
opacity: 1;
transform: none;
filter: none;
}
}
@media (prefers-reduced-motion: reduce) {
.reading-progress-highlight > * {
opacity: 1;
transform: none;
animation: none;
}
}import { useEffect, useRef } from "react";
/** Reading Progress Highlight — trigger: scroll position within the article */
export function ReadingProgressHighlight({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
el.dataset["state"] = "in";
return;
}
const io = new IntersectionObserver(
([entry]) => {
if (entry?.isIntersecting) {
el.dataset["state"] = "in";
io.unobserve(el);
}
},
{ threshold: 0.2 },
);
io.observe(el);
return () => io.disconnect();
}, []);
return (
<div ref={ref} className="reading-progress-highlight" data-state="out">
{children}
</div>
);
}Key terms in a paragraph brighten after the copy settles.
List items slide in with their markers drawing first.
Content blocks appear in stages as the reader advances.
Sentences brighten in turn so attention moves down the block.