How to do a blur-in image reveal with CSS
An image that just pops into place feels abrupt. A image that resolves — starting soft and slightly too bright, then snapping into focus — feels like it’s tuning in. On my portfolio the profile photo does exactly that, matching the whole “old CRT display coming to life” aesthetic. It’s a single CSS keyframe. Here it is.
The keyframe
/* src/index.css */
.image-reveal {
animation: imageReveal 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
@keyframes imageReveal {
from {
filter: blur(12px) brightness(2);
transform: scale(1.04);
}
to {
filter: blur(0) brightness(1);
transform: scale(1);
}
}That’s the entire effect. Let me break down each moving part, because every one earns its place.
filter: blur(12px)→blur(0)— the core of the reveal. The image starts heavily blurred and sharpens to crisp. This is what your eye reads as “coming into focus.”brightness(2)→brightness(1)— it also starts at double brightness and settles to normal. Pairing an over-bright start with the blur gives that glowing, washed-out “signal locking in” look rather than just a plain smudge sharpening.transform: scale(1.04)→scale(1)— a barely-there zoom-out. Starting 4% larger and settling to actual size adds a subtle physical “settle” so the frame doesn’t feel static while the filter animates.
Why these specific settings
The duration is short — 0.6s. A blur reveal that lingers reads as slow or broken; you want it quick enough to feel responsive but slow enough to notice.
The easing is cubic-bezier(0.22, 1, 0.36, 1) — the same decelerate-hard curve I use elsewhere on the site. It rushes toward sharpness early and eases gently into the final frame, so the focus “lands” softly instead of stopping dead.
And forwards (the fill-mode) is what keeps the image sharp after the animation finishes. Without it, the browser would drop back to the element’s base styles the moment the animation ended — and since the from state is the blurred one, an unset base could flash back to blur. forwards freezes the final to frame in place.
When it runs
The important design decision is not running the reveal at page load. On my hero section the photo is hidden behind a fake “NO SIGNAL / RECEIVING DATA” loader, and only when the intro timeline reaches the reveal step do I let the real image show and sharpen. That timing is driven by state:
// src/components/portfolio/HeroSection.tsx (the image element)
<img
src="/ehsan-profile-416.webp"
alt="Portrait of Ehsan Shahid"
className={
timeline >= 7
? "opacity-100 grayscale contrast-110 brightness-90"
: "opacity-0"
}
/>The image is invisible until the timeline hits the reveal step, so the blur-in coincides with the section’s “boot” sequence finishing rather than firing before anyone’s looking. The lesson generalizes: a reveal animation is only as good as its timing — gate it on the moment you actually want the eye drawn there (load complete, element on screen, or a step in a sequence), not on nothing.
If you wanted the plainer “run once the image finishes loading” version, you’d add the .image-reveal class in the <img>’s onLoad handler instead of tying it to a timeline. Same keyframe, different trigger.
What to remember
- A convincing focus effect is just
filter: blur()animating toblur(0). - Pair it with a brightness fade and a tiny
scalesettle for a richer “tuning in” feel. - Keep it short (~0.6s) and use a decelerating
cubic-bezierso it lands softly. - Use
forwardsso the image stays sharp after the animation ends. - Gate the trigger on the right moment — image loaded, on screen, or a sequence step — not on page load.