BLUR TO FOCUS
INTERACTION_031CSS
Blur To Focus Reveal
Copy resolves from a soft blur into perfect focus as it enters view.
Text / LightView
The workhorse: a paragraph lifts gently into place with no drama. 0 opacity, 14px below final position before the trigger; translating to 0 and fading to full opacity once the paragraph enters view.
/* Fade Up Paragraph — The workhorse: a paragraph lifts gently into place with no drama. */
.fade-up-paragraph {
--dur: 450ms;
--ease: cubic-bezier(0.22, 1, 0.36, 1);
--stagger: 0ms;
will-change: transform, opacity;
}
.fade-up-paragraph > * {
opacity: 0;
transform: translateY(14px);
animation: fade-up-paragraph-in var(--dur) var(--ease) forwards;
animation-delay: calc(var(--i, 0) * var(--stagger));
}
/* 0 opacity, 14px below final position -> translating to 0 and fading to full opacity */
@keyframes fade-up-paragraph-in {
to {
opacity: 1;
transform: none;
filter: none;
}
}
@media (prefers-reduced-motion: reduce) {
.fade-up-paragraph > * {
opacity: 1;
transform: none;
animation: none;
}
}import { useEffect, useRef } from "react";
/** Fade Up Paragraph — trigger: the paragraph enters view */
export function FadeUpParagraph({ 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="fade-up-paragraph" data-state="out">
{children}
</div>
);
}Copy resolves from a soft blur into perfect focus as it enters view.
Each line slides up from behind an invisible mask, one after the other.
Words fade up in sequence so the sentence assembles as you read it.
Sentences in a block arrive one at a time with a calm rhythm.